Member von Strukturen per SFINAE löschen



  • So die nächste Variante ohne die Nachteile von vorher.

    #include <type_traits>
    #include <iostream>
    
    template<class T>
    class Data;
    
    template<class T>
    requires std::is_same_v<T, void>
    class Data<T> {
    public:
        int Id = 0;
    };
    
    template<class T>
    requires (! std::is_same_v<T, void>)
    class Data<T> {
    public:
        int Id = 0;
        T t = 1;
    };
    
    int main() {
        Data<int>  d1; // OK
        Data<void> d2; // OK
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    


  • Vielen Dank für die verschiedenen Ansätze und Lösungen!

    Ich habe aktuell kein konkretes Problem, für das ich das hier brauche, die Frage entstand aus Neugier. Leider habe ich im Moment auch wenig Zeit, als dass ich die verschiedenen Lösung ausprobieren könnte, ich werde aber drauf zurückkommen, wenn ich mich damit beschäftigt habe.



  • @john-0 sagte in Member von Strukturen per SFINAE löschen:

    So die nächste Variante ohne die Nachteile von vorher.

    #include <type_traits>
    #include <iostream>
    
    template<class T>
    class Data;
    
    template<class T>
    requires std::is_same_v<T, void>
    class Data<T> {
    public:
        int Id = 0;
    };
    
    template<class T>
    requires (! std::is_same_v<T, void>)
    class Data<T> {
    public:
        int Id = 0;
        T t = 1;
    };
    
    int main() {
        Data<int>  d1; // OK
        Data<void> d2; // OK
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    

    Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?
    Das macht es doch nochmal um ein vielfaches cleaner.

    template<class T>
    class Data{
    public:
        int Id = 0;
        T t = 1;
    };
    
    template<>
    class Data<void> {
    public:
        int Id = 0;
    };
    


  • @DNKpp sagte in Member von Strukturen per SFINAE löschen:

    Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?

    Es würde nach eine SFINAE Lösung gefragt, und bei SFINAE kann man eben nicht nur den Typen als Unterscheidung nutzen, wie bei der Spezialisierung. Nehmen wir mal an, dass wir eine andere Anforderung haben z.B. dass der Datentyp die Bedingungen eines Körpers erfüllt.

    #include <type_traits>
    #include <iostream>
    #include <string>
    
    template <typename T>
    concept Addition = requires (T a, T b) {a + b;};
    
    template <typename T>
    concept Substraction = requires (T a, T b) {a - b;};
    
    template <typename T>
    concept Multiplication = requires (T a, T b) {a * b;};
    
    template <typename T>
    concept Division = requires (T a, T b) {a / b;};
    
    template <typename T>
    concept Field = Addition<T> && Substraction<T> && Multiplication<T> && Division<T>
      && std::regular<T>;
    
    
    template<class T>
    class Data;
    
    template<class T>
    requires (!Field<T>)
    class Data<T> {
    public:
        int Id = 0;
    };
    
    template<class T>
    requires Field<T>
    class Data<T> {
    public:
        int Id = 0;
        T t = 1;
    };
    
    int main() {
        Data<int>  d1;        // OK
        Data<std::string> d2; // OK
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    


  • @john-0 sagte in Member von Strukturen per SFINAE löschen:

    @DNKpp sagte in Member von Strukturen per SFINAE löschen:

    Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?

    Es würde nach eine SFINAE Lösung gefragt, und bei SFINAE kann man eben nicht nur den Typen als Unterscheidung nutzen, wie bei der Spezialisierung. Nehmen wir mal an, dass wir eine andere Anforderung haben z.B. dass der > Datentyp die Bedingungen eines Körpers erfüllt.

    Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden. Immerhin prüfst du auf "hat Eigenschaft" und "hat Eigenschaft nicht". Das mag für dieses binäre Verhalten noch klar gehen, skaliert aber echt schlecht.

    template<class T>
    class Data {
    public:
        int Id = 0;
    };
    
    template<Field T>
    class Data<T> {
    public:
        int Id = 0;
        T t = 1;
    };
    


  • @DNKpp sagte in Member von Strukturen per SFINAE löschen:

    Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden.

    Es ist der Weg, den sich die Macher von C++ ausgedacht haben. Es ist nicht so effizient wie

    template<typename T>
    struct Data {
        int Id = 0;
    #ifdef T_IS_NOT_VOID
        T t = 1;
    #endif
    };
    

    . Es geht auch nicht über constexpr if

    #include <iostream>
    
    template <typename T>
    struct Data {
        int Id = 0;
    
        if constexpr (std::is_same_v<T, void>) {
            T t = 1;
        }
    };
    
    int main() {
        Data<int>  d1;
        Data<void> d2;
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    

    Man kann in vielen Fällen constexpr if verwenden, aber nicht in einem Fall in dem man einen Member definieren will.



  • Außerdem löschen eure Lösungen doch gar nicht den Member, sondern ihr kopiert einfach die Klasse und fügt in der "non-void"-Klasse den zusätzlichen Member hinzu.
    Es geht ja darum bei einer beliebigen Klasse (mit beliebig vielen Membern und Memberfunktionen) einen (oder mehrere) Member zu "löschen" (d.h. keinen Speicherplatz dafür zu verschwenden).

    Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten

    [[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
    

    benutzen.



  • @john-0 sagte in Member von Strukturen per SFINAE löschen:

    @DNKpp sagte in Member von Strukturen per SFINAE löschen:

    Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden.

    Es ist der Weg, den sich die Macher von C++ ausgedacht haben. Es ist nicht so effizient wie

    Es steht doch nirgendwo geschrieben, dass ein Primary Template immer undefiniert sein muss. Es ist ein Fakt, dass es nicht sinnvoll ist, eine Bedingung zu überprüfen und andernfalls die gegenteilige Bedingung nochmal zu prüfen.

    Aber mach du nur, wie du willst. Ich hab dir einen alternativen und kürzeren Weg gezeigt. Mehr wollte ich gar nicht.

    @Th69 sagte in Member von Strukturen per SFINAE löschen:

    Außerdem löschen eure Lösungen doch gar nicht den Member, sondern ihr kopiert einfach die Klasse und fügt in der "non-void"-Klasse den zusätzlichen Member hinzu.
    Es geht ja darum bei einer beliebigen Klasse (mit beliebig vielen Membern und Memberfunktionen) einen (oder mehrere) Member zu "löschen" (d.h. keinen Speicherplatz dafür zu verschwenden).

    Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten

    [[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
    

    benutzen.

    Dafür hast du halt hier einen Member, der nichts tut und ggf eher irritiert. Hat doch alles seine Vor- und Nachteile. Zumal das no_unique_address Attribut afaik noch gar nicht von allen Compilern supportet wird (msvc bietet z.b. msvc::no_unique_address als Alternative an).



  • @Th69 sagte in Member von Strukturen per SFINAE löschen:

    Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten

    [[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
    

    benutzen.

    Ausprobiert?
    Ich bekomme mit

    #include <type_traits>
    #include <iostream>
    #include <variant>
    
    template <typename T, bool C = std::is_same_v<T, void>>
    struct Data {
        int Id = 0;
    
        [[no_unique_address]] std::conditional_t<C, T, std::monostate> m;
    };
    
    int main() {
        Data<int>  d1; // OK
        Data<void> d2; // OK
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    

    folgende Ausgabe (2. Fehler interessiert nicht)

    sfinae5.cc: In instantiation of ‘struct Data<void>’:
    sfinae5.cc:14:13:   required from here
    sfinae5.cc:9:72: error: ‘Data<T, C>::m’ has incomplete type
        9 |         [[no_unique_address]] std::conditional_t<C, T, std::monostate> m;
          |                                                                        ^
    sfinae5.cc:9:72: error: invalid use of ‘std::conditional_t<true, void, std::monostate>’ {aka ‘void’}
    sfinae5.cc: In function ‘int main()’:
    sfinae5.cc:16:52: error: ‘struct Data<int>’ has no member named ‘t’
       16 |         std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
          |                                                    ^
    


  • @john-0 sagte in Member von Strukturen per SFINAE löschen:

    std::conditional_t<C, T, std::monostate> m;

    Probiers doch mal mit std::conditional_t<!C, T, std::monostate> m; 🙂



  • Manchmal sieht man die trivialsten Dinge nicht. Und es muss an dieser Stelle sein, da es in der Template Namendefinition nicht funktioniert.



  • @DNKpp sagte in Member von Strukturen per SFINAE löschen:

    Es steht doch nirgendwo geschrieben, dass ein Primary Template immer undefiniert sein muss. Es ist ein Fakt, dass es nicht sinnvoll ist, eine Bedingung zu überprüfen und andernfalls die gegenteilige Bedingung nochmal zu prüfen.

    Der Aspekt stört dich, ok berechtiger Einwand. Ich hatte es per Cut & Paste aus anderem Code übernommen in dem A oder B gültig sein musste, da nicht A und nicht B als Fall nicht übersetzen dürfen. Ich dachte, Du störst Dich an etwas anderem.

    Also korrigiert

    #include <type_traits>
    #include <iostream>
    #include <string>
    
    template <typename T>
    concept Addition = requires (T a, T b) {a + b;};
    
    template <typename T>
    concept Substraction = requires (T a, T b) {a - b;};
    
    template <typename T>
    concept Multiplication = requires (T a, T b) {a * b;};
    
    template <typename T>
    concept Division = requires (T a, T b) {a / b;};
    
    template <typename T>
    concept Field = Addition<T> && Substraction<T> && Multiplication<T> && Division<T>
      && std::regular<T>;
    
    
    template<class T>
    class Data {
    public:
        int Id = 0;
    };
    
    template<class T>
    requires Field<T>
    class Data<T> {
    public:
        int Id = 0;
        T t = 1;
    };
    
    int main() {
        Data<int>  d1; // OK
        Data<std::string> d2; // OK
    
        std::cout << "d1: " << d1.Id << ", " << d1.t << "\n";
        std::cout << "d2: " << d2.Id << "\n";
    }
    


  • [[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
    

    Interessante Lösung. Mein Problem damit wäre aber, dass während der Member zwar keinen zusätzlichen Speicher benötigt, er in der Programmlogik immer noch vorhanden ist. Das mag je nach Anwendungsfall kein Problem sein, ich könnte mir aber vorstellen, dass ich bei so einem Design vielleicht irgendwo ein Concept haben wollte, das mir für einen beliebigen "Data"-Typen sagt, ob dieser einen Context hat:

    if constexpr (has_context<decltype(data)>)
        do_something_with_context(data);
    

    Sowas wäre dann weniger geradlinig zu implementieren, zumal das vielleicht auch nicht immer ein std::monostate ist, sondern vielleicht auch mal ein struct MyEmptyContext {};.

    Ich persönlich finde da einen auch in der Programmlogik nicht vorhandenen Member eleganter und Vererbung nicht wirklich verwerflich (Data ist ein potentiell Context-haltendes Objekt) - ja in diesem Fall sogar leichter verständlich. "Komposition statt Vererbung" ist erstens keine harte Regel, sondern eine Empfehlung, diese zu bevorzugen und zweitens macht die m.E. vornehmlich bei tiefen Klassenhierarchien Sinn. Den Data-Zweig sehe ich hier nicht als solche. Da wird die Vererbungshierarchie sehr wahrscheinlich ohnehin nach ein, zwei Schritten bei Data terminiert und dieses dann vermutlich wiederum als Member (Komposition) in irgendeiner anderen Klasse verwendet.

    Aus dem Grund sehe ich hier nicht nur die Vererbung unkritisch, sondern erachte auch Polymorphie als Overkill. Ich sehe Data eher als Lowlevel-Utility-Klasse, die auch Teil einer relativ generischen Bibliothek sein könnte. Da lasse ich andere Regeln gelten, als für große Toplevel-Klassenhierarchien, die "Geschäftslogik" abbilden (und die ich wohl auch "per Default" polymorph machen würde).

    Nur meine persönlich Ansicht, für ich hier nochmal argumentieren wollte - ich bestehe da nicht wirklich drauf. Wählt die Lösung mit der ihr glücklicher seid 😉


Anmelden zum Antworten