METAProgramming: Rückgabewert einer Methode absichern



  • Hi zusammen

    ich möchte in einem UserInterface ein Funktionstemplate anbieten, welches bestimmte Eigenschaften der verfügbaren Methoden von 'T' absichert/festlegt.

    Als Beispiel: ich möchte sicherstellen dass die Methode 'method()' auch wirklich den Rückgabewert 'uint64_t' hat.
    Der Hintergrund ist: in meinem Anwendungsfall verbirgt sich hinter 'doSomething' eine Methode, die etwas in einen Stream schreibt.
    Bekanntermaßen sind Streams ( z.B. std::stringstream ) ja nicht besonders wählerisch was Typen angeht.

    Kurz gesagt: ich will dort nicht alles akzeptieren was einen StreamingOperator hat.

    Ich habe das mal versucht mit Metaprogrammierung zu erschlagen, aber bisher kompiliert es nicht, was vermutlich mit std::result_of zu tun hat ( inklusive meiner mangelnden META-Erfahrung 😃 ).

    #include <cstdint>
    
    class TestClassSuccess
    {
    public:
        uint64_t method() { return 1; }
    };
    
    class TestClassFailed
    {
    public:
        int method() { return 1; }
    };
    
    template <typename T, typename = std::enable_if_t<( std::is_same_v<
                                                                       std::result_of<decltype(T::method())&()>::type,
                                                                       uint64_t
                                                                      > )>>
    void doSomething( const T &obj )
    {
    }
    
    int main(int , char *[])
    {
        TestClassSuccess foo;
        doSomething( foo );
        return 0;
    }
    

    gruß Tobi



  • Versuchs mal so:

    template <typename T, typename = std::enable_if_t<
      std::is_same_v<decltype(std::declval<T>().method()), uint64_t >>>
    void doSomething( const T &obj )
    {
    }
    


  • Danke funzt 🙂



  • Mich würde mal noch interessieren, ob man sowas auch mit C++20 concepts erschlagen kann.



  • @It0101 sagte in METAProgramming: Rückgabewert einer Methode absichern:

    Mich würde mal noch interessieren, ob man sowas auch mit C++20 concepts erschlagen kann.

    Sicher doch, sogar eleganter und lesbarer IMHO:

    #include <cstdint>
    #include <concepts>
    #include <iostream>
    
    struct A
    {
        auto method() const -> std::uint64_t
        {
            return 64;
        }
    };
    
    struct B
    {
        auto method() const -> int
        {
            return 32;
        }
    };
    
    struct C
    {
        auto other_method() const -> int;
    };
    
    template <typename T>
    concept has_uint64_const_method = requires(const T object) 
    {
        { object.method() } -> std::same_as<std::uint64_t>;
    };
    
    template <typename T>
    concept has_uint64_convertible_const_method = requires(const T object) 
    {
        { object.method() } -> std::convertible_to<std::uint64_t>;
    };
    
    void write(const has_uint64_const_method auto& object)
    {
        std::cout << "write " << object.method() << std::endl;
    }
    
    void write_convertible(const has_uint64_convertible_const_method auto& object)
    {
        std::cout 
            << "write_convertible " 
            << static_cast<std::uint64_t>(object.method())
            << std::endl;
    }
    
    auto main() -> int
    {
        std::cout 
            << std::boolalpha
            << "has_const_uint64_method<A> = " 
                << has_uint64_const_method<A> << "\n"
            << "has_const_uint64_method<B> = " 
                << has_uint64_const_method<B> << "\n"
            << "has_const_uint64_method<C> = " 
                << has_uint64_const_method<C> << std::endl;
        std::cout 
            << std::boolalpha
            << "has_const_uint64_convertible_method<A> = "
                << has_uint64_convertible_const_method<A> << "\n"
            << "has_const_uint64_convertible_method<B> = "
                << has_uint64_convertible_const_method<B> << "\n"
            << "has_const_uint64_convertible_method<C> = "
                << has_uint64_convertible_const_method<C> << std::endl;
        write(A{});
        write_convertible(A{});
        write_convertible(B{});
    }
    

    https://godbolt.org/z/cGeT5scxz

    Prüft sogar, ob die Methode auch const ist, was die TMP-Lösung nicht tut, wenn ich das richtig sehe (std::declval<const T>().method() sollte das tun). Ansonsten wird doSomething(const T &obj) Probleme machen, wenn die Methode nicht const ist.

    Alternativ den Constraint auch direkt an die Funktion kleben, ohne ein Concept einzuführen (z.B. wenn man es eh nur an einer Stelle braucht):

    template <typename T>
        requires requires(const T object) 
        {
            { object.method() } -> std::same_as<std::uint64_t>;
        }
    void doSomething(const T& object)
    {
        ...
    }
    


  • Hab das jetzt so übernommen. Concepts gefällt mir. Endlich mal ein schöner Anwendungsfall dafür 🙂

    Kann mir jemand noch sagen, ob man in einem Concept auch mit "or"-Verknüpfungen arbeiten kann, wenn man z.B. einen von zwei unterschiedlichen Typen erzwingen will ?



  • Man kann die Clauses im require statement mit && oder || verknüpfen.

    Mit require wird vieles beim Template Metaprogramming erheblich simpler zu formulieren und geradliniger zu implementieren.



  • Für Beispiele siehe z.b. https://en.cppreference.com/w/cpp/language/constraints
    Conjunctions (&&) und Disjunctions(||)



  • Ah. Der Begriff "Conjunction" hat mir gefehlt zum Googeln 😃

    Ich hatte es auf diese Art versucht:

        template <typename T>
        concept isvalidtype = requires(const T &object)
        {
            { object.method() } -> ( std::same_as<std::uint64_t> || std::same_as<std::string> );
        };
    

    Aber dann mach ichs halt auf die andere Art 🙂



  • Ich habe es jetzt ungefähr so:

    
    
        template <typename T>
        concept is_valid_method1 = requires(const T &object)
        {
            { object.method1() } -> std::same_as<std::uint64_t>;
        };
    
        template <typename T>
        concept is_float_method2 = requires(const T &object)
        {
            { object.method2() } -> std::floating_point;
        };
    
        template <typename T>
        concept is_string_method2 = requires(const T &object)
        {
            { object.method2() } -> std::same_as<std::string>;
        };
    
    
        template <typename T>
        concept isvalidtype = ( is_valid_method1<T> && ( is_float_method2<T> || is_string_method2<T> ) );
    

    Ein finales "Concept" welches sich aus einzelnen zusammensetzt. Würde man das so machen, oder geht das noch kürzer ohne die lesbarkeit zu verlieren?



  •     template <typename T>
        concept ValidValueType = std::floating_point<T> || std::same_as<T,std::string>;
    
        template <typename T>
        concept is_val = requires(const T &object)
        {
            { object.method1() } -> std::same_as<std::uint64_t>;
            { object.method2() } -> ValidValueType;
        };
    

    Ok so sieht es schon besser aus.



  • @It0101 Eventuell ist std::same_as<std::uint64_t> ein wenig zu restriktiv, das gilt z.B. nicht, wenn die Methode ein std::uint64_t-Referenz zurückgibt. Das könnte man mit so einem Concept beheben (dass es in der Form glaube ich nicht in der Standardbibliothek gibt):

    template <typename T, typename U>
    concept same_as_without_cvref = std::same_as<std::remove_cvref_t<T>,  std::remove_cvref_t<U>>;
    

    Oder aber - was ich in solchen Fällen persönlich bevorzuge - du prüfst auf std::convertible_to<std::uint64_t> und machst ein static_cast<std::uint64_t>(object.method()) wenn du den std::uint64_t-Wert auslesen willst. Das erlaubt ein bisschen mehr Freiheiten, wie T::method() implementiert wird. Natürlich nur, wenn du keine andere, gleichnamige Funktion aufrufen willst, falls es sich z.B. um ein (nach std::uint64_t konvertierbaren) std::uint32_t handelt und das zu Mehrdeutigkeiten führen würde.

    Was hier wirklich Sinn macht, solltest du jedoch letztendlich selbst wissen 🙂



  • @Finnegan sagte in METAProgramming: Rückgabewert einer Methode absichern:

    same_as_without_cvref

    Auf genau das Problem bin ich auch gestoßen. Letztendlich geht es mir ja nur um den Wert selbst. (const-)referenzen sind ebenfalls zulässig.

    Ich war erst bei "std::remove_reference" was aber irgendwie nicht gefruchtet hat. Evtl. hab ich es auch falsch eingesetzt. Danke für dein Beispiel. 😁



  • @It0101 sagte in METAProgramming: Rückgabewert einer Methode absichern:

    @Finnegan sagte in METAProgramming: Rückgabewert einer Methode absichern:

    same_as_without_cvref

    Auf genau das Problem bin ich auch gestoßen. Letztendlich geht es mir ja nur um den Wert selbst. (const-)referenzen sind ebenfalls zulässig.

    Ich war erst bei "std::remove_reference" was aber irgendwie nicht gefruchtet hat. Evtl. hab ich es auch falsch eingesetzt. Danke für dein Beispiel. 😁

    std::remove_cvref_t entfernt auch noch const/volatile, so dass nur noch der nackte Type übrig bleibt. is_same macht auch bei unterschiedlichen cv-Qualifikationen einen Unterschied, allerdings wird im Type Constraint (nach dem ->) der Typ decltype(<expression>)
    geprüft, wobei cv-Qualifikationen entfernt werden, so dass eigentlich auch remove_reference_t reichen sollte.

    Ansonsten: convertible_to ist zu liberal für deine Anforderungen? Ich kenn den Rest des Codes nicht, aber bei solchen Dingen wäre bei mir convertible_to + Cast in meinen gewünschten Typ erstmal immer Default. Aber es kann sein, dass du z.B. eine int-Methode anders behandeln möchtest, dann macht das natürlich so Sinn (Edit: Hah! Grad gemerkt, dass ich jetzt schon zum dritten Mal mit convertible_to rumnerve, das wird wohl tatsächlich einen guten Grund haben, dass du das nicht verwendest... sorry 🙂 ).



  • @Finnegan sagte in METAProgramming: Rückgabewert einer Methode absichern:

    Ansonsten: convertible_to ist zu liberal für deine Anforderungen? Ich kenn den Rest des Codes nicht, aber bei solchen Dingen wäre bei mir convertible_to + Cast in meinen gewünschten Typ erstmal immer Default. Aber es kann sein, dass du z.B. eine int-Methode anders behandeln möchtest, dann macht das natürlich so Sinn.

    Ja genau. Ich möchte explizit einen 64Bit-Typ erzwingen. Es handelt sich in meinem Anwendungsfall um einen UTC-Timestamp in Millisekunden seit Epoche, daher möchte ich, um Fehler durch den Nutzer zu vermeiden, direkt den großen Typ erzwingen, den ich auch intern verwende. Daher fällt "convertible_to" raus. 🙂
    Aber dennoch danke für den Hinweis. 😉


Anmelden zum Antworten