Klon af Boost Variant

Som en del af læring af C ++, med særlig vægt på C ++ 11, ønskede jeg at implementere det ækvivalente af Boosts Variant (placeret her ). Min kode er tilgængelig på variant.hpp med den aktuelle version nedenfor .

Hvordan kan std::aligned_storage bruges bærbart? Min nuværende løsning gør sandsynligvis ikke-bærbar brug af static_cast hvis den er bærbar, ville denne information være meget værdifuld. Den bestemte kode svarer til *static_cast<T*>(static_cast<void*>(&value)), for value af typen typename std::aligned_storage<...>::type (hvor ... ikke er beregnet til at angive variadiske skabeloner).

Jeg bruger noget af static_assert. I denne særlige brug, ville SFINAE være bedre? Jeg forstår SFINAE kan bruges til at beskære overbelastninger fra sættet af levedygtige funktioner, men hvor jeg bruger static_assert I antage der ville kun være en levedygtig funktion, selvom jeg ville finde værdifulde eksempler på tilfælde, hvor der er mere end en levedygtig funktion.

Jeg brugte meget std::forward. Er det muligt at klare sig med færre anvendelser?

Jeg brugte std::enable_if på en af konstruktørens overbelastninger for at sikre, at den kun ville blive brugt, når et træk er beregnet (se variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Uden begge enable_if s ville denne konstruktor blive brugt, når kopikonstruktøren variant(variant const&) i stedet er beregnet til, selvom førstnævnte resulterer i en eventuel kompilatorfejl. Er der en bedre måde at tvinge denne adfærd på? En løsning, jeg prøvede, var at inkludere variant(variant&) som en overbelastning, der bare overgår til variant(variant const& rhs) – den ville blive valgt over variant(U&&), mens variant(U&&) foretrækkes frem for variant(variant const&) af overbelastningsreglerne. Hvad er den generelle bedste praksis, når du bruger T&& til nogle nyligt introducerede T når der er tænkt på at flytte semantik i stedet for en universel reference?

Jeg har stadig brug for at tilføje multivisitorer, selvom jeg i almindelighed har nogle problemer med dette (ved hjælp af variadiske skabeloner). Noget interessant, der kom op ved implementering af variant klassen var implicitte konverteringer mellem variant, der kun involverede omarrangering af skabelonargumenterne, eller hvor lvalue-skabelonen argumenter er et supersæt af rvalue-skabelonargumenterne.

Alle kommentarer, spørgsmål eller råd er meget værdsat.

#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 

Kommentarer

  • I henhold til vores hjælpecenter er dette ikke stedet for kode, der ikke ‘ ikke gør hvad det skal gør. Fungerer din kode ?
  • Hvis du ved ” “, mener du, ” kompilerer den? “, kompilerer den med clang ++ version 3.3, men kompilerer ikke med g ++ version 4.8.2 på grund af en interaktion mellem parameterpakker og lambdas. CMakeLists.txt-filen vælger clang ++ ubetinget. Hvis du ved ” arbejder “, mener du ” er den bærbar? “, så nej, det er muligvis ikke bærbart. Dette skyldes den måde, jeg bruger std::aligned_storage på. Råd om bærbar brug af den tilknyttede type vil være meget nyttigt. Hvis du ved ” arbejder “, mener du ” tilfredsstiller den den oprindelige hensigt? “, svaret er ja, selvom jeg meget gerne vil have stilistiske og bedste råd.
  • Brug af std::forward ser fint ud, jeg kan ‘ ikke se noget sted, hvor du ikke kunne bruge det. Jeg foretrækker static_assert, da det giver dig mulighed for at give bedre fejlmeddelelser. Bærbarhed er potentielt et problem, da der ikke er nogen garanti for, at alle markører er justeret på den samme grænse (så void * og T* kan have forskellige krav ), men det ville være meget usædvanligt i disse dage. Jeg ved ikke ‘ hvordan boost kommer omkring dette, eller hvis de bare ignorerer det.
  • @Yuushi, først tak, fordi du tog dig tid til at gennemgå dette . Jeg tror, de bare ignorerer ikke-bærbare ting ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Jeg fandt en bærbar løsning ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ), der bruger en teknik svarende til, hvad std::tuple bruger, men med union.
  • Intet problem.Den eneste anden ting, jeg ‘ tilføj, er, at filen virker lidt ” optaget ” og der er ting i det, der potentielt er nyttige i sig selv (træk, som du ‘ har defineret som is_movable og enable_if_movable, f.eks.) Der potentielt kunne bo et andet sted. Desværre er antallet af C ++ korrekturlæsere her omkring med den viden, der er nødvendig for at give dig god feedback på dette, sandsynligvis i de enkelte cifre, da koden er ret kompleks.

Svar

Der er meget her, så jeg vil dele min anmeldelse i stykker. Jeg vil starte med bare at fokusere på metafunktionsafsnittet. Metafunktioner kan være korte, men de er meget stærke og vigtige for at få ret – men med hensyn til korrekthed og brugbarhed.

Til at begynde med:

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>; 

Den første er simpelthen forkert. Du bruger denne metafunktion til at kontrollere, om en type er flytbar (i move_construct) … men du gør dette ved blot at kontrollere, om det hverken er en lvalue-reference eller const. Du kontrollerer faktisk ikke noget, der vedrører flytningskonstruktion. Bare fordi noget er en værdi for en værdi, betyder det ikke, at du kan flytte fra det. Og bare fordi noget er en værdi for en værdi, betyder det ikke, at du ikke kan. Overvej to enkle klasser:

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

Som navnet antyder, er No ikke flytbar. Din metafunktion siger, at den er. Også, Yes& er bevægelig konstruerbar, men din metafunktion siger nej.

Den korrekte implementering ville være at bruge standardtypeegenskaben std::is_move_constructible .

For det andet er aliaset der tvivlsomt. Vi bruger typisk aliasser til at undgå at skulle skriv typename ::type cruft. Det gør du ikke, og det resulterende opkald er ikke så meget mere kortfattet. Sammenlign:

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 

Jeg foretrækker den sidste version personligt. Bemærk, at jeg bruger C ++ 14-aliaset her. Hvis du ikke har en C ++ 14-kompilator, er det absolut værd at starte dit metafunktionsbibliotek med dem alle. De skal bare skrive:

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

Gå videre til:

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

Der er ingen måde, at nogen vil vide, hvad elem gør her. Jeg gjorde det ikke, før jeg læste implementeringen. Et meget bedre navn på dette ville være contains. Men jeg kommer tilbage til implementeringen om et øjeblik.

Lad os starte med:

template <bool... List> struct all; 

all er super nyttigt. Det samme er dens nære slægtninge any og none. Den måde, du skrev all er fint og fungerer, men gør ikke det er lettere at skrive de to andre. En god måde at skrive disse ud på er at bruge @Columbo “s bool_pack trick:

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

That” s bare din hjælper. Du kan bruge det til at implementere resten let:

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>; 

Og når vi først har det, kan vi genimplementere contains som en linie:

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

På samme måde som før ser jeg ikke værdien i enable_if_elem Og common_result_of skal tage typen og ikke kun give metafunktionen:

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

Selv om det er mere læsbart at bare holde det i din variant selv:

// 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)>...>; 

Nu til brugen . I det hele taget bruger du disse metafunktioner i returtypen:

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

Eller som en dummy-markør:

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

Men i begge tilfælde finder jeg det meget nemmere at analysere komplekse skabelonudtryk, hvis du sætter SFINAE-logikken som en unavngiven endelig skabelonparameter:

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); 

Konsistensen hjælper også med at forstå. Dummy pointer-argumentet er en forvirrende hackrester fra C ++ 03. Der er ikke noget behov for det længere. Især når du har brug for to dummy-markører:

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

Sidebemærkning til denne fyr. Det gør faktisk ikke, hvad du vil. Dette er ikke en vilkårlig referenceramme – det er en videresendingsreference. Faktisk kan vi endda kombinere de to konstruktører her på én gang:

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)); } 

Bemærk, at dette også løser et andet problem med din kode – nemlig at du aldrig kontrollerer hvis en klasse kan kopieres. Hvad hvis du ville holde noget som unique_ptr i din variant. Flyt konstruktivt, men ikke kopier konstruktivt – men du har aldrig kontrolleret det i din kode. Vigtigt – ellers ville dine brugere bare få kryptiske fejlmeddelelser.

Jeg tror, dette afslutter metafunktionsdelen. Jeg vil skrive en anmeldelse af variant sig selv lidt senere.Håber du finder det nyttigt.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *