Default-Initialisierung eines void* mit -1
-
Hallo zusammen,
wir versuchen gerade von C++11 auf C++17 zu migrieren und stolpern dabei über Kleinigkeiten. Ich hoffe zumindest, dass das Kleinigkeiten sind, eine davon ist zB das hier:
Ich habe eine Klasse, die RAII für Windows Objekte implementiert (alle möglichen Arten von Handles, etc). Das ist alles etwas umfangreicher, aber lässt sich auf dieses Beispiel reduzieren:#include <memory> // typedefs aus windows.h, zu Demozwecken hier definiert using HANDLE = void*; #define INVALID_HANDLE_VALUE -1 template<typename ObjectType, ObjectType InvalidObjectValue> class SharedObject { struct Holder { ObjectType Object = InvalidObjectValue; Holder() = default; }; std::shared_ptr<Holder> Holder_; public: SharedObject() = default; }; using SharedWin32Handle = SharedObject<HANDLE, INVALID_HANDLE_VALUE>; int main() { SharedWin32Handle test; return 0; }
Die Fehlermeldung sind compilerspezifisch, der Code auf Wandbox mit folgender Fehlermeldung quittiert:
prog.cc:19:48: error: conversion from 'int' to 'void*' in a converted constant expression 19 | using SharedObject<HANDLE, INVALID_HANDLE_VALUE> = SharedWin32Handle; | ^ prog.cc:19:48: error: could not convert '-1' from 'int' to 'void*' prog.cc:19:7: error: expected nested-name-specifier 19 | using SharedObject<HANDLE, INVALID_HANDLE_VALUE> = SharedWin32Handle;
Bei einigen Windows Funktionen ist der Rückgabewert ein HANDLE, der Fehlschlag wird aber unterschiedlich gekennzeichnet. Einerseits wird 0 bzw nullptr zurückgegeben, in anderen Fällen INVALID_HANDLE_VALUE bzw. -1. Mein Template soll als Template Parameter den ungültigen Wert besitzen, und was mit C++11 noch ging geht in C++17 jetzt nicht mehr (wohl aus alignment-Gründen).
Lange Rede, kurzer Sinn: Wie kann ich dem Compiler klarmachen, dass der ungültige Wert für ein HANDLE -1 ist? Es läuft ja auf diese Zuweisung hinaus:void* ptr = -1;
Edit:
using-Anweisung gefixt
-
Mittels
reinterpret_cast<void*>(-1)
bzw.reinterpret_cast<HANDLE>(INVALID_HANDLE_VALUE)
.
Am besten, du legst dafür eine eigene Konstante an:const HANDLE InvalidHandleValue = reinterpret_cast<HANDLE>(INVALID_HANDLE_VALUE)
und verwendest diese dann im Template.
-
Nö, das geht auch nicht, da meckert der Compiler, dass
reinterpret_cast
in einem konstanten Ausdruck nicht erlaubt ist.
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Mein Template soll als Template Parameter den ungültigen Wert besitzen, und was mit C++11 noch ging geht in C++17 jetzt nicht mehr (wohl aus alignment-Gründen).
Lange Rede, kurzer Sinn: Wie kann ich dem Compiler klarmachen, dass der ungültige Wert für ein HANDLE -1 ist? Es läuft ja auf diese Zuweisung hinaus:void* ptr = -1;
Ich sehe hier kein Alignment-Problem, das wenn, dann eh erst bei der Dereferenzierung eines solchen Pointers relevant ein dürfte. Dass der Wert es Pointers ein Problem macht, ist mit bisher noch nicht untergeommen. Ich habe sogar schonmal die untersten 2 Bits bei Pointern mit 4-Byte-Alignment genutzt, um platzsparend Meta-Informationen zu speichern (muss man natürlich vor dem dereferenzieren ausmaskieren).
Hier kann soweit ich sehe lediglich ein
int
nicht einfach so implizit in einen Pointer-Typen konvertiert werden.Wie wäre es mit:
void* ptr = reinterpret_cast<void*>(std::intptr_t{ -1 });
Das sollte eigentlich funktionieren. Oder übersehe ich hier was Grundlegendes?
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Nö, das geht auch nicht, da meckert der Compiler, dass
reinterpret_cast
in einem konstanten Ausdruck nicht erlaubt ist.Oh, wieder was gelernt. Ist mir in dem Kontext wohl noch nicht untergekommen bisher
... ich schau mal obs da irgendeine Trickserei gibt, das muss doch gehen. Wie würde man eigentlich einen
nullptr
definieren, wenns ihn nicht gäbe?
-
@Finnegan
Keine Ahnung ob du was übersiehst, aber leider funktioniert dein Ansatz auch nicht.
C++Shell :Quellcode
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
@Finnegan
Keine Ahnung ob du was übersiehst, aber leider funktioniert dein Ansatz auch nicht.
C++Shell :QuellcodeIm Zweifel eine Spezialisierung von
SharedObject
für denObjectType
HANDLE
, die intern mitintptr_t
/uintptr_t
arbeitet und an den richtigen Stellen während der Laufzeit in einHANDLE
castet. Aber das ist natürlich deutlich mehr Aufwand als wenn diese eine Zeile einfach nur azeptiert würde
-
Oder ich nehme einen weiteren Template Parameter dazu, der den Typ des ungültigen Wertes bestimmt:
template<typename ObjectType, typename InvalidObjectType, InvalidObjectType InvalidObjectValue> class { ... };
Trotzdem ist das Ganze iwie merkwürdig. Warum geht das mit C++17 plötzlich nicht mehr?
-
Es ist bei C++17 wohl nur noch
nullptr
als direkte Adresszuweisung (als Template-Parameter) erlaubt.Der MSVC nimmt es aber weiterhin (nur CLANG und GCC nicht): Godbolt Code.
-
Eien Pointer kannst du dir mit memcpy erzeugen:
void* make_invalid_ptr() { void *ptr; uintptr_t i = -1; std::memcpy(&ptr, &i, sizeof ptr); return ptr; }
(ungetestet)
-
Aber nicht als Template-Parameter!
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Trotzdem ist das Ganze iwie merkwürdig. Warum geht das mit C++17 plötzlich nicht mehr?
Der Standard sagt offenbar schon länger, dass u.a. das Resultat von
reinterpret_cast
keine Constant Expression ist. Scheinbar waren die Compiler vorher etwas toleranter in der Auslegung.Ich habe jetzt nach einiger Sucherei auch keine Ein-Zeilen-Lösung gefunden und würde das selbst wahrscheinlich so lösen:
#include <memory> #include <cstdint> using HANDLE = void*; template <typename ObjectType> struct SharedObjectTraits; template <> struct SharedObjectTraits<HANDLE> { static HANDLE invalid_value; }; HANDLE SharedObjectTraits<HANDLE>::invalid_value = reinterpret_cast<HANDLE>(std::intptr_t{ -1 }); template <typename ObjectType> class SharedObject { struct Holder { ObjectType Object = SharedObjectTraits<ObjectType>::invalid_value; }; std::shared_ptr<Holder> Holder_; public: SharedObject() = default; }; using SharedWin32Handle = SharedObject<HANDLE>; int main() { SharedWin32Handle test; return 0; }
Die Traits-Klasse hilft, die Anzahl der notwenigen Template-Parameter zu reduzieren (finde ich beser zu lesen, da es weniger Code-Rauschen erzeugt). Man kann aber auch einen "Invalid Value"-Typen mit so einem Static Member als Template-Parameter übergeben, wenn dir das sinnvoller erscheint.
Der Ansatz ist insofern auch besser, als dass er generell nicht auf Constant Expressions angewiesen ist, was die ungültigen Werte angeht. Vielleicht hat man es ja auch mal mit einem
ObjectType
zu tun, wo völlig klar ist, dass man den Wert nicht durch einenconstexpr
-Ausdruck schieben kann. Z.B. wenninvalid_value
aus einer Bibliothek kommt und man nur viaextern
-Variable darauf zugreifen kann.Edit:
invalid_value
muss natürlich einstatic
Member sein.
-
Danke Finnegan, aber mit type traits funktioniert das leider nicht. Die WINAPI ist da leider inkonsistent, manche Funktionen, die ein
HANDLE
als Rückgabewert haben, liefern 0 (nullptr) zurück, andere -1 (INVALID_HANDLE_VALUE). Den ungültigen Wert per type trait zu bestimmen geht nicht, weil ich anhand des HANDLE nicht bestimmen kann, welcher der beiden Werte jetzt gebraucht wird.
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Danke Finnegan, aber mit type traits funktioniert das leider nicht. Die WINAPI ist da leider inkonsistent, manche Funktionen, die ein
HANDLE
als Rückgabewert haben, liefern 0 (nullptr) zurück, andere -1 (INVALID_HANDLE_VALUE). Den ungültigen Wert per type trait zu bestimmen geht nicht, weil ich anhand des HANDLE nicht bestimmen kann, welcher der beiden Werte jetzt gebraucht wird.Dann übergebe den "Invalid Value"-Typen einfach per Template-Parameter:
template <typename ObjectType, typename InvalidValueType> class SharedObject { ... ObjectType Object = InvalidValueType::invalid_value; }; ... using SharedWin32Handle = SharedObject<HANDLE, SharedObjectTraits<HANDLE>>; // oder welchen Typen auch immer: using SharedWin32Handle2 = SharedObject<HANDLE, InvalidValueType2>; using SharedWin32Handle3 = SharedObject<HANDLE, InvalidValueType3>;
Praktisch ist auch, dass man in der Klasse, die man als zusätzlichen Template-Parameter übergibt (oder als Traits-Klasse definiert), z.B. auch den Code unterbringen kann, um das Handle wieder freizugeben. Ich vermute mal da gibt es in Win32 auch vercheidene Funtionen für, je nachem, was für eine Art Handle es ist (?).
-
Und da beißt sich die Katze wieder in den Schwanz:
Code auf C++ShellHat den Fehler jetzt aus der Holder-Klasse in die Type-Traits Klasse verschoben
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Und da beißt sich die Katze wieder in den Schwanz:
Code auf C++ShellHat den Fehler jetzt aus der Holder-Klasse in die Type-Traits Klasse verschoben
Ich hatte in meiner Traits-Lösung das hier verlinkt (unter "so lösen").
Du kannst nicht implizit von
int
nachvoid*
konvertieren. Du muss das schon explizit machen.Und btw.: Es sind zwei Fehler hier. Einmal die implizite Konvertierung und dann (was mich auch überrascht hat) dass
reinterpret_cast
(die naheliegende Lösung für die Konvertierung) nicht imconstexpr
-Kontext erlaubt ist.
-
Jau, geht
Jetzt muss ich mir mal Zeit nehmen und gucken, ob sich das so umsetzen lässt.
-
@Finnegan sagte in Default-Initialisierung eines void* mit -1:
Praktisch ist auch, dass man in der Klasse, die man als zusätzlichen Template-Parameter übergibt (oder als Traits-Klasse definiert), z.B. auch den Code unterbringen kann, um das Handle wieder freizugeben. Ich vermute mal da gibt es in Win32 auch vercheidene Funtionen für, je nachem, was für eine Art Handle es ist (?).
Ja, für sowas habe ich Allocator-Klassen, die als Template Parameter übergeben werden. Tun hier aber nichts zur Sache, also habe ich sie ausgelassen.
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
@Finnegan sagte in Default-Initialisierung eines void* mit -1:
Praktisch ist auch, dass man in der Klasse, die man als zusätzlichen Template-Parameter übergibt (oder als Traits-Klasse definiert), z.B. auch den Code unterbringen kann, um das Handle wieder freizugeben. Ich vermute mal da gibt es in Win32 auch vercheidene Funtionen für, je nachem, was für eine Art Handle es ist (?).
Ja, für sowas habe ich Allocator-Klassen, die als Template Parameter übergeben werden. Tun hier aber nichts zur Sache, also habe ich sie ausgelassen.
Macht es vielleicht Sinn,
invalid_value
zu einemstatic
Member dieser Klassen zu machen? Nur so als Anregung - das könnte den Code etwas kompakter machen und einiges an Boilerplate reduzieren. Welcher Handle-Wert ungültig ist, hängt doch sicher an den Funktionen, mit denen man das Objekt, auf welches das Handle verweist, erstellt, a.k.a "Allokations"-Funktionen, wenn ich das richtig verstehe (?). Das klingt für mich erstmal so, als obinvalid_value
vom Design her gut in diese Allocator-Klassen passen würde.
-
Geht sogar noch einfacher
Ich habe ja vorhin die Allocator-Klasse erwähnt, die für die Freigabe des gehaltenen Objekts verantwortlich ist. Die bekommt jetzt einfach noch ne statische Methode, die den ungültigen Objekttyp zurückgibt.
Im ersten Ansatz sähe das jetzt so aus:#include <memory> struct HandleAllocator { static void release( HANDLE object ) { CloseHandle( object ); } static HANDLE invalid_object_value() { return INVALID_HANDLE_VALUE; } }; template<typename ObjectType, typename Allocator> class SharedObject { struct Holder { ObjectType Object = Allocator::invalid_object_value(); Holder() = default; Holder( ObjectType object ) { Object = object; } ~Holder() { Allocator::release( Object ); } }; std::shared_ptr<Holder> Holder_; ... }; using Win32SharedHandle = SharedObject<HANDLE, HandleAllocator>; int main() { Win32SharedHandle sh; }
-
@DocShoe sagte in Default-Initialisierung eines void* mit -1:
Geht sogar noch einfacher
Ich habe ja vorhin die Allocator-Klasse erwähnt, die für die Freigabe des gehaltenen Objekts verantwortlich ist. Die bekommt jetzt einfach noch ne statische Methode, die den ungültigen Objekttyp zurückgibt.Finde ich sogar noch besser als Funktion. Flexibler und simplere Syntax.
P.S.: Wenn du die Funktion jetzt übrigens noch
constexpr
machst, dann schliesst sich der Kreis wieder und wir können die Diskussion von vorne beginnen... SCNR
-
@Finnegan sagte in Default-Initialisierung eines void* mit -1:
P.S.: Wenn du die Funktion jetzt übrigens noch
constexpr
machst, dann schliesst sich der Kreis wieder und wir können die Diskussion von vorne beginnen... SCNRIch hab´ Zeit