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
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.
std::aligned_storage
på. Råd om bærbar brug af den tilknyttedetype
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.std::forward
ser fint ud, jeg kan ‘ ikke se noget sted, hvor du ikke kunne bruge det. Jeg foretrækkerstatic_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 *
ogT*
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.std::tuple
bruger, men medunion
.is_movable
ogenable_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.