Klon der Boost-Variante

Als Teil des Lernens von C ++, mit besonderem Schwerpunkt auf C ++ 11, wollte ich das Äquivalent der Boost-Variante (lokalisiert) implementieren hier ). Mein Code ist unter varianten.hpp mit der unten angegebenen aktuellen Version verfügbar

Wie kann std::aligned_storage portabel verwendet werden? Meine aktuelle Lösung verwendet jedoch static_cast wahrscheinlich nicht portabel Wenn es portabel ist, wären diese Informationen sehr wertvoll. Der bestimmte Code ähnelt *static_cast<T*>(static_cast<void*>(&value)) für value vom Typ typename std::aligned_storage<...>::type (wobei ... keine variablen Vorlagen anzeigen soll).

Ich verwende static_assert. Wäre SFINAE in dieser speziellen Verwendung besser? Ich verstehe, dass SFINAE verwendet werden kann, um Überlastungen aus dem Satz funktionsfähiger Funktionen zu entfernen, aber wo ich static_assert I verwende dort annehmen wäre nur eine realisierbare Funktion, obwohl ich wertvolle Beispiele für Fälle finden würde, in denen es mehr als eine realisierbare Funktion gibt.

Ich habe std::forward häufig verwendet. Ist es möglich, mit weniger Verwendungen auszukommen?

Ich habe std::enable_if für eine der Konstruktorüberladungen verwendet, um sicherzustellen, dass sie nur bei einer Verschiebung verwendet wird ist vorgesehen (siehe variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Ohne die beiden enable_if s würde dieser Konstruktor verwendet, wenn der Kopierkonstruktor variant(variant const&) stattdessen beabsichtigt ist, obwohl der erstere zu einem führt eventueller Compilerfehler. Gibt es einen besseren Weg, um dieses Verhalten zu erzwingen? Eine Lösung, die ich versucht habe, war das Einfügen von variant(variant&) als Überladung, die sich nur auf variant(variant const& rhs) verlagert – sie würde über , während variant(U&&) von den Überladungsregeln gegenüber variant(variant const&) bevorzugt wird. Was ist die allgemeine bewährte Methode, wenn T&& für einige neu eingeführte T verwendet wird, wenn eine Verschiebungssemantik anstelle einer universellen Referenz beabsichtigt ist?

Ich muss noch Multivisitoren hinzufügen, obwohl ich im allgemeinen Fall Probleme damit habe (unter Verwendung verschiedener Vorlagen). Interessant bei der Implementierung der Klasse variant waren implizite Konvertierungen zwischen variant, bei denen nur die Vorlagenargumente neu angeordnet wurden oder die Vorlage lvalue neu angeordnet wurde Argumente sind eine Obermenge der Argumente der r-Wert-Vorlage.

Alle Kommentare, Fragen oder Ratschläge werden sehr geschätzt.

#ifndef WART_VARIANT_HPP #define WART_VARIANT_HPP #include <type_traits> #include <utility> #include "math.hpp" namespace wart { template <typename... T> class variant; namespace detail { namespace variant { template <typename... T> using variant = wart::variant<T...>; template <typename T> using is_movable = typename std::integral_constant <bool, std::is_rvalue_reference<T&&>::value && !std::is_const<T>::value>; template <typename T, typename U = void> using enable_if_movable = std::enable_if<is_movable<T>::value, U>; template <typename... Types> using union_storage = typename std::aligned_storage <math::max_constant<std::size_t, sizeof(Types)...>::value, math::lcm_constant<std::size_t, std::alignment_of<Types>::value...>::value>::type; template <typename... Types> using union_storage_t = typename union_storage<Types...>::type; template <typename Elem, typename... List> struct elem; template <typename Head, typename... Tail> struct elem<Head, Head, Tail...>: std::true_type {}; template <typename Elem, typename Head, typename... Tail> struct elem<Elem, Head, Tail...>: elem<Elem, Tail...>::type {}; template <typename Elem> struct elem<Elem>: std::false_type {}; template <typename Elem, typename... List> struct elem_index; template <typename Head, typename... Tail> struct elem_index<Head, Head, Tail...>: std::integral_constant<int, 0> {}; template <typename Elem, typename Head, typename... Tail> struct elem_index<Elem, Head, Tail...>: std::integral_constant<int, elem_index<Elem, Tail...>::value + 1> {}; template <bool... List> struct all; template <> struct all<>: std::true_type {}; template <bool... Tail> struct all<true, Tail...>: all<Tail...>::type {}; template <bool... Tail> struct all<false, Tail...>: std::false_type {}; template <typename Elem, typename... List> using enable_if_elem = std::enable_if<elem<Elem, List...>::value>; template <typename F, typename... ArgTypes> using common_result_of = std::common_type<typename std::result_of<F(ArgTypes)>::type...>; struct destroy { template <typename T> void operator()(T&& value) { using type = typename std::remove_reference<T>::type; std::forward<T>(value).~type(); } }; struct copy_construct { void* storage; template <typename T> void operator()(T const& value) { new (storage) T(value); } }; template <typename... T> struct copy_construct_index { void* storage; template <typename U> int operator()(U const& value) { new (storage) U(value); return elem_index<U, T...>::value; } }; struct move_construct { void* storage; template <typename T> typename enable_if_movable<T>::type operator()(T&& value) { new (storage) T(std::move(value)); } }; template <typename... T> struct move_construct_index { void* storage; template <typename U> typename enable_if_movable<U, int>::type operator()(U&& value) { new (storage) U(std::move(value)); return elem_index<U, T...>::value; } }; struct copy_assign { void* storage; template <typename T> void operator()(T const& value) { *static_cast<T*>(storage) = value; } }; template <typename... T> struct copy_assign_reindex { variant<T...>& variant; template <typename U> void operator()(U const& value) { if (variant.which_ == elem_index<U, T...>::value) { *static_cast<U*>(static_cast<void*>(&variant.storage_)) = value; } else { variant.accept(destroy{}); new (&variant.storage_) U(value); variant.which_ = elem_index<U, T...>::value; } } }; struct move_assign { void* storage; template <typename T> typename enable_if_movable<T>::type operator()(T&& value) { *static_cast<T*>(storage) = std::move(value); } }; template <typename... T> struct move_assign_reindex { variant<T...>& variant; template <typename U> typename enable_if_movable<U>::type operator()(U&& value) { if (variant.which_ == elem_index<U, T...>::value) { *static_cast<U*>(static_cast<void*>(&variant.storage_)) = std::move(value); } else { variant.accept(destroy{}); new (&variant.storage_) U(std::move(value)); variant.which_ = elem_index<U, T...>::value; } } }; } } template <typename... T> class variant { int which_; detail::variant::union_storage_t<T...> storage_; public: template <typename F> using result_of = detail::variant::common_result_of<F, T...>; template <typename F> using result_of_t = typename result_of<F>::type; template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr): which_{detail::variant::elem_index<U, T...>::value} { new (&storage_) U(value); } template <typename U> variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr): which_{detail::variant::elem_index<U, T...>::value} { new (&storage_) U(std::move(value)); } variant(variant const& rhs): which_{rhs.which_} { rhs.accept(detail::variant::copy_construct{&storage_}); } template <typename... U> variant(variant<U...> const& rhs, typename std::enable_if< detail::variant::all<detail::variant::elem<U, T...>::value...>::value >::type* = nullptr): which_{rhs.accept(detail::variant::copy_construct_index<T...>{&storage_})} {} variant(variant&& rhs): which_{rhs.which_} { std::move(rhs).accept(detail::variant::move_construct{&storage_}); } template <typename... U> variant(variant<U...>&& rhs, typename std::enable_if< detail::variant::all<detail::variant::elem<U, T...>::value...>::value >::type* = nullptr): which_{std::move(rhs).accept(detail::variant::move_construct_index<T...>{&storage_})} {} ~variant() { accept(detail::variant::destroy{}); } variant& operator=(variant const& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); if (this == &rhs) { return *this; } if (which_ == rhs.which_) { rhs.accept(copy_assign{&storage_}); } else { accept(destroy{}); rhs.accept(copy_construct{&storage_}); which_ = rhs.which_; } return *this; } template <typename... U> variant& operator=(variant<U...> const& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); rhs.accept(copy_assign_reindex<T...>{*this}); return *this; } variant& operator=(variant&& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_move_constructible<T>::value...>::value, "all template arguments T must be nothrow move constructible in class template variant"); if (this == &rhs) { return *this; } if (which_ == rhs.which_) { std::move(rhs).accept(move_assign{&storage_}); } else { accept(detail::variant::destroy{}); std::move(rhs).accept(move_construct{&storage_}); which_ = rhs.which_; } return *this; } template <typename... U> variant& operator=(variant<U...>&& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); std::move(rhs).accept(move_assign_reindex<T...>{*this}); return *this; } template <typename F> result_of_t<F> accept(F&& f) const& { using namespace detail::variant; using call = result_of_t<F&&> (*)(F&& f, union_storage_t<T...> const&); static call calls[] { [](F&& f, union_storage_t<T...> const& value) { return std::forward<F>(f)(*static_cast<T const*>(static_cast<void const*>(&value))); }... }; return calls[which_](std::forward<F>(f), storage_); } template <typename F> result_of_t<F> accept(F&& f) & { using namespace detail::variant; using call = result_of_t<F&&> (*)(F&& f, union_storage_t<T...>&); static call calls[] { [](F&& f, union_storage_t<T...>& value) { return std::forward<F>(f)(*static_cast<T*>(static_cast<void*>(&value))); }... }; return calls[which_](std::forward<F>(f), storage_); } template <typename F> result_of_t<F> accept(F&& f) && { using namespace detail::variant; using call = result_of_t<F> (*)(F&& f, union_storage_t<T...>&&); static call calls[] { [](F&& f, union_storage_t<T...>&& value) { return std::forward<F>(f)(std::move(*static_cast<T*>(static_cast<void*>(&value)))); }... }; return calls[which_](std::forward<F>(f), std::move(storage_)); } friend struct detail::variant::copy_assign_reindex<T...>; friend struct detail::variant::move_assign_reindex<T...>; }; } #endif 

Kommentare

  • Auch in unserer Hilfe ist dies nicht die Site für Code, der ‚ nicht das tut, was er soll machen. Funktioniert Ihr Code ?
  • Wenn mit “ “ funktioniert, meinen Sie, “ wird kompiliert? “ kompiliert es mit clang ++ Version 3.3, kompiliert jedoch aufgrund einer Interaktion nicht mit g ++ Version 4.8.2 zwischen Parameterpaketen und Lambdas. Die Datei CMakeLists.txt wählt bedingungslos clang ++ aus. Wenn mit “ “ gearbeitet wird, meinen Sie “ ist es portabel? „, dann nein, es ist möglicherweise nicht portabel. Dies liegt an der Art und Weise, wie ich std::aligned_storage verwende. Ratschläge zur tragbaren Verwendung des zugehörigen type wären sehr hilfreich. Wenn mit “ “ gearbeitet wird, meinen Sie “ erfüllt es die ursprüngliche Absicht? “ lautet die Antwort ja, obwohl ich stilistische und bewährte Ratschläge sehr gerne hätte.
  • Die Verwendung von std::forward sieht gut aus, ich kann ‚ keinen Ort sehen, an dem Sie es nicht verwenden könnten. Ich bevorzuge static_assert, da Sie so bessere Fehlermeldungen geben können. Die Portabilität ist möglicherweise ein Problem, da nicht garantiert werden kann, dass alle Zeiger an derselben Grenze ausgerichtet sind (void * und T* haben möglicherweise unterschiedliche Anforderungen ), aber das wäre heutzutage sehr ungewöhnlich. Ich ‚ weiß nicht, wie Boost dies umgeht oder ob sie es einfach ignorieren.
  • @Yuushi, zuerst danke, dass Sie sich die Zeit genommen haben, dies zu überprüfen . Ich denke, sie ignorieren einfach alle nicht portablen Dinge ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Ich habe eine tragbare Lösung gefunden ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ), die eine ähnliche Technik verwendet wie std::tuple, jedoch mit union.
  • Kein Problem.Das einzige andere, was ich ‚ hinzufügen würde, ist, dass die Datei ein bisschen “ beschäftigt “ und es gibt Dinge darin, die möglicherweise für sich genommen nützlich sind (Eigenschaften, die Sie ‚ definiert haben, wie is_movable und enable_if_movable zum Beispiel), die möglicherweise woanders leben könnten. Leider ist die Anzahl der C ++ – Prüfer hier mit dem Wissen, das erforderlich ist, um Ihnen ein gutes Feedback dazu zu geben, wahrscheinlich einstellig, da der Code ziemlich komplex ist.

Antwort

Es gibt hier eine Menge, also werde ich meine Rezension in Teile aufteilen. Ich möchte mich zunächst nur auf den Metafunktionsbereich konzentrieren. Metafunktionen mögen kurz sein, aber sie sind „sehr mächtig und wichtig, um richtig zu sein – aber in Bezug auf Korrektheit und Nützlichkeit.

Zunächst:

template <typename T> using is_movable = typename std::integral_constant <bool, std::is_rvalue_reference<T&&>::value && !std::is_const<T>::value>; template <typename T, typename U = void> using enable_if_movable = std::enable_if<is_movable<T>::value, U>; 

Der erste ist einfach falsch. Sie verwenden diese Metafunktion, um zu überprüfen, ob ein Typ konstruierbar verschoben werden kann (in move_construct) … aber Sie tun dies indem Sie nur überprüfen, ob es sich weder um eine Wertreferenz noch um const handelt. Sie überprüfen eigentlich nichts in Bezug auf die Verschiebungskonstruktion. Nur weil etwas eine Wertreferenz ist, heißt das nicht, dass Sie sich davon entfernen können. Und nur weil etwas eine Wertreferenz ist, heißt das nicht, dass Sie es nicht können. Betrachten Sie zwei einfache Klassen:

struct No { A(A&& ) = delete; }; struct Yes { }; 

Wie der Name schon sagt, ist No nicht konstruierbar. Ihre Metafunktion sagt dies auch. Yes& ist verschiebbar konstruierbar, aber Ihre Metafunktion sagt nein.

Die richtige Implementierung wäre, einfach das Standardtypmerkmal zu verwenden std::is_move_constructible .

Zweitens ist der Alias dort fraglich. Normalerweise würden wir Aliase verwenden, um zu vermeiden, dass müssen Schreiben Sie die Cruft typename ::type. Sie tun das nicht, und der resultierende Anruf ist nicht viel prägnanter. Vergleichen Sie:

typename enable_if_movable<T>::type // yours with alias std::enable_if_t<is_moveable<T>::value> // just using enable_if without alias std::enable_if_t<std::is_move_constructible<T>::value> // just stds 

Ich würde die letzte Version persönlich bevorzugen. Beachten Sie, dass ich hier den C ++ 14-Alias verwende. Wenn Sie keinen C ++ 14-Compiler haben, lohnt es sich unbedingt, Ihre Metafunktionsbibliothek mit allen zu starten. Sie sind einfach zu schreiben:

template <bool B, typename T = void> using enable_if_t = typename std::enable_if<B, T>::type; 

Weiter zu:

template <typename Elem, typename... List> struct elem; 

Es gibt keine So kann jeder wissen, was elem hier tut. Ich habe es nicht getan, bis ich die Implementierung gelesen habe. Ein viel besserer Name dafür wäre contains. Aber ich werde gleich zur Implementierung zurückkehren.

Beginnen wir zunächst mit:

template <bool... List> struct all; 

all ist sehr nützlich. Ebenso die nahen Verwandten any und none. Die Art und Weise, wie Sie all geschrieben haben, ist in Ordnung und funktioniert, funktioniert aber nicht es ist einfacher, die anderen beiden zu schreiben. Eine gute Möglichkeit, diese zu schreiben, besteht darin, den bool_pack -Trick von @Columbo zu verwenden:

template <bool...> struct bool_pack; template <bool f, bool... bs> using all_same = std::is_same<bool_pack<f, bs...>, bool_pack<bs..., f>>; 

Das „s nur dein Helfer. Sie können dies verwenden, um den Rest einfach zu implementieren:

template <bool... bs> using all_of = all_same<true, bs...>; template <bool... bs> using none_of = all_same<false, bs...>; template <bool... bs> using any_of = std::integral_constant<bool, !none_of<bs...>::value>; 

Und sobald wir das haben, können wir contains als Einzeiler:

template <typename Elem, typename... List> using contains = any_of<std::is_same<Elem, List>::value...>; 

Ähnlich wie zuvor sehe ich den Wert in enable_if_elem nicht Und common_result_of sollte den Typ annehmen und nicht nur die Metafunktion ergeben:

template <typename F, typename... ArgTypes> using common_result_of = std::common_type_t<std::result_of_t<F(ArgTypes)>::...>; 

Obwohl es besser lesbar ist, dies einfach in Ihr variant selbst zu stecken:

// no need to use anything in detail::, unless you need to // write your own aliases for common_type_t and result_of_t template <typename F> using result_of = std::common_type_t<std::result_of_t<F(T)>...>; 

Nun zur Verwendung . Währenddessen verwenden Sie diese Metafunktionen im Rückgabetyp:

template <typename T> typename enable_if_movable<T>::type operator()(T&& value); 

Oder als Dummy-Zeiger:

template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr) 

In beiden Fällen ist es jedoch viel einfacher, komplexe Vorlagenausdrücke zu analysieren, wenn Sie die SFINAE-Logik als unbenannten endgültigen Vorlagenparameter verwenden:

template <typename T, typename = std::enable_if_t<std::is_move_constructible<T>::value> > void operator()(T&& value); template <typename U, typename = std::enable_if_t<contains<U, T...>::value> > variant(U const& value); 

Die Konsistenz hilft auch beim Verständnis. Das Dummy-Zeiger-Argument ist ein verwirrender Hack-Rest aus C ++ 03. Es besteht keine Notwendigkeit mehr. Besonders wenn Sie zwei Dummy-Zeiger benötigen:

 template <typename U, typename = std::enable_if_t<contains<U, T...>::value && std::is_move_constructible<U>::value> > variant(U&& value); 

Randnotiz zu diesem Typen macht eigentlich nicht was du willst. Dies ist keine willkürliche Wertreferenz, sondern eine Weiterleitungsreferenz. Tatsächlich können wir die beiden Konstruktoren hier sogar auf einmal kombinieren:

template <typename U, typename V = std::remove_reference_t<U>, typename = std::enable_if_t<contains<V, T...>::value && std::is_constructible<V, U&&>::value> > variant(U&& value) : which_{elem_index<V, T...>::value} { new (&storage_) V(std::forward<U>(value)); } 

Beachten Sie, dass dies auch ein anderes Problem mit Ihrem Code löst – nämlich, dass Sie es nie überprüfen Wenn eine Klasse kopierkonstruierbar ist. Was wäre, wenn Sie so etwas wie unique_ptr in Ihre Variante einfügen möchten? Verschiebbar verschieben, aber nicht konstruierbar kopieren – aber Sie haben das nie in Ihrem Code überprüft. Wichtig – andernfalls würden Ihre Benutzer nur kryptische Fehlermeldungen erhalten.

Ich denke, dies schließt den Metafunktionsabschnitt ab. Ich werde etwas später eine Rezension über variant selbst schreiben.Ich hoffe, Sie finden das hilfreich.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.