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.



  • @Th69

    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:

    @Th69

    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 :Quellcode

    Im Zweifel eine Spezialisierung von SharedObject für den ObjectType HANDLE, die intern mit intptr_t/uintptr_t arbeitet und an den richtigen Stellen während der Laufzeit in ein HANDLE 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 einen constexpr-Ausdruck schieben kann. Z.B. wenn invalid_value aus einer Bibliothek kommt und man nur via extern-Variable darauf zugreifen kann.

    Edit: invalid_value muss natürlich ein static 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++Shell

    Hat 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++Shell

    Hat 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 nach void* 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 im constexpr-Kontext erlaubt ist.



  • @Finnegan

    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 einem static 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 ob invalid_value vom Design her gut in diese Allocator-Klassen passen würde.



  • @Finnegan

    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:

    @Finnegan

    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... SCNR 😛

    Ich hab´ Zeit 😉


Anmelden zum Antworten