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 wirddoSomething(const T &obj)
Probleme machen, wenn die Methode nichtconst
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 einstd::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 einstatic_cast<std::uint64_t>(object.method())
wenn du denstd::uint64_t
-Wert auslesen willst. Das erlaubt ein bisschen mehr Freiheiten, wieT::method()
implementiert wird. Natürlich nur, wenn du keine andere, gleichnamige Funktion aufrufen willst, falls es sich z.B. um ein (nachstd::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 nochconst
/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 Typdecltype(<expression>)
geprüft, wobei cv-Qualifikationen entfernt werden, so dass eigentlich auchremove_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 mirconvertible_to
+ Cast in meinen gewünschten Typ erstmal immer Default. Aber es kann sein, dass du z.B. eineint
-Methode anders behandeln möchtest, dann macht das natürlich so Sinn (Edit: Hah! Grad gemerkt, dass ich jetzt schon zum dritten Mal mitconvertible_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 mirconvertible_to
+ Cast in meinen gewünschten Typ erstmal immer Default. Aber es kann sein, dass du z.B. eineint
-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.