Clone of Boost Variant (Čeština)

V rámci výuky jazyka C ++, se zvláštním důrazem na C ++ 11, jsem chtěl implementovat ekvivalent Boostovy varianty (nachází se zde ). Můj kód je k dispozici na variant.hpp , přičemž aktuální verze je uvedena níže .

Jak lze std::aligned_storage použít přenosně? Moje současné řešení pravděpodobně používá static_cast nepřenosné použití pokud jsou přenosné, byly by tyto informace velmi cenné. Konkrétní kód je podobný *static_cast<T*>(static_cast<void*>(&value)) pro value typu typename std::aligned_storage<...>::type (kde ... nemá znamenat variadické šablony).

Určitě využívám static_assert. Bylo by v tomto konkrétním použití lepší SFINAE? Chápu, že SFINAE lze použít k odstranění přetížení ze sady životaschopných funkcí, ale tam, kde používám static_assert I předpokládej tam by byla pouze jedna životaschopná funkce, i když bych považoval za cenné všechny příklady případů, kdy existuje více než jedna životaschopná funkce.

Hodně jsem využil std::forward. Je možné vystačit s menším počtem použití?

Použil jsem std::enable_if na jednom z přetížení konstruktoru, abych zajistil, že bude použit pouze při pohybu je zamýšleno (viz variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Bez obou enable_if s by se tento kontruktor použil, když je místo toho určen konstruktor kopírování variant(variant const&), přestože první má za následek eventuální chyba kompilátoru. Existuje lepší způsob, jak toto chování vynutit? Jedním z řešení, které jsem zkoušel, bylo zahrnutí variant(variant&) jako přetížení, které právě delguje na variant(variant const& rhs) – bylo by vybráno přes variant(U&&), zatímco variant(U&&) je podle pravidel přetížení upřednostňován před variant(variant const&). Jaký je obecný nejlepší postup při použití T&& pro některé nově zavedené T, když je namísto univerzálního odkazu určena sémantika přesunu?

Stále potřebuji přidat multivisitory, i když s tím mám obecně potíže (pomocí variadic šablon). Něco zajímavého, co se objevilo při implementaci třídy variant, byly implicitní převody mezi variant s, které zahrnovaly pouze přeskupení argumentů šablony nebo kde šablona lvalue argumenty jsou nadmnožinou argumentů šablony rvalue.

Všechny komentáře, dotazy nebo rady jsou velmi ceněny.

#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 

Komentáře

  • Podle našeho centra nápovědy nejde o web pro kód, který by ‚ nedělal to, co měl dělat. Funguje váš kód ?
  • Pokud “ funguje „, máte na mysli, “ kompiluje se? „, kompiluje se s clang ++ verze 3.3, ale nekompilovává se s g ++ verze 4.8.2 kvůli interakci mezi balíčky parametrů a lambdami. Soubor CMakeLists.txt vybere clang ++ bezpodmínečně. Pokud “ prací „, máte na mysli “ přenosný? „, pak ne, nemusí být přenosný. Důvodem je způsob, jakým používám std::aligned_storage. Velmi užitečné by byly rady ohledně přenosného použití type. Pokud “ prací „, máte na mysli “ splnění původního záměru? „, odpověď je ano, i když bych velmi rád získal stylistické a osvědčené rady.
  • Použití std::forward vypadá dobře, ‚ nevidím žádné místo, kde byste jej nemohli použít. Dávám přednost static_assert, protože vám umožňuje poskytovat lepší chybové zprávy. Přenositelnost je potenciálně problém, protože neexistuje žádná záruka, že všechny ukazatele budou zarovnány na stejné hranici (void * a T* proto mohou mít odlišné požadavky ), ale v dnešní době by to bylo velmi neobvyklé. Nevím ‚ nevím, jak to obejde podpora, nebo jestli to prostě ignorují.
  • @Yuushi, nejprve vám děkuji, že jste si našli čas na kontrolu . Myslím, že ignorují všechny nepřenosné věci ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Našel jsem přenosné řešení ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ), který používá techniku podobnou té, kterou std::tuple používá, ale s union.
  • Žádný problém.Jediná další věc, kterou ‚ d přidám, je, že soubor vypadá trochu “ zaneprázdněný “ a jsou v něm věci, které jsou samy o sobě potenciálně užitečné (vlastnosti, které jste ‚ definovali jako is_movable a enable_if_movable), které by potenciálně mohly žít někde jinde. Bohužel počet recenzentů C ++ se znalostmi potřebnými k poskytnutí dobré zpětné vazby je pravděpodobně jednociferný, protože kód je poměrně složitý.

Odpověď

Tady je spousta, takže svoji recenzi rozdělím na kousky. Chci začít pouhým zaměřením na metafunkční sekci. Metafunkce mohou být krátké, ale „jsou velmi silné a důležité je napravit – ale pokud jde o správnost a užitečnost.

Na začátek:

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

První je prostě špatně. Tuto metafunkci používáte ke kontrole, zda je typ přesunutelný konstruktivně (v move_construct) … ale děláte to pouhou kontrolou, zda to není ani odkaz na hodnotu, ani const. Ve skutečnosti nekontrolujete nic, co by se týkalo konstrukce pohybu. Jen proto, že je něco referencí rvalue, neznamená, že se z něj můžete přesunout. A to, že je něco referencí lvalue, neznamená, že nemůžete. Zvažte dvě jednoduché třídy:

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

Jak název napovídá, No se nepohne konstruktivně. Vaše metafunkce říká, že ano. Také Yes& je konstruovatelný, ale vaše metafunkce říká ne.

Správnou implementací by bylo jednoduše použít standardní typový znak std::is_move_constructible .

Zadruhé, alias je sporný. Aliasy obvykle používáme k vyhnutí nutnosti napište typename ::type cruft. „To neděláte a výsledný hovor není o moc výstižnější. Porovnat:

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 

Osobně bych dal přednost poslední verzi. Všimněte si, že zde používám alias C ++ 14. Pokud nemáte kompilátor C ++ 14, rozhodně stojí za to spustit metafunkční knihovnu se všemi. Mají jednoduše psát:

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

Přesun na:

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

Neexistuje žádný způsobem, že někdo bude vědět, co zde elem dělá. Dokud jsem implementaci nečetl, nedělal jsem to. Mnohem lepší název by byl contains. K implementaci se ale za chvíli vrátím.

Nejprve začněme:

template <bool... List> struct all; 

all je velmi užitečný. Stejně tak jeho blízcí příbuzní any a none. Způsob, jakým jste napsali all, je v pořádku a funguje, ale nedělá to je snazší napsat další dva. Dobrým způsobem, jak je vypsat, je použít trik @Columbo „s bool_pack:

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

To je jen tvůj pomocník. Můžete to použít k snadné implementaci všech ostatních:

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

A jakmile to máme, můžeme znovu implementovat contains jako jednorázový:

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

Podobně jako dříve nevidím hodnotu v enable_if_elem . A common_result_of by měl převzít typ , nejen přinést metafunkci:

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

I když je mnohem čitelnější, když to prostě vložíte do svého variant samotného:

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

Nyní k použití . V celém rozsahu používáte tyto metafunkce v návratovém typu:

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

Nebo jako atrapa ukazatele:

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

Ale v obou případech považuji za mnohem snazší analyzovat složité výrazy šablony, pokud vložíte logiku SFINAE jako nepojmenovaný konečný parametr šablony:

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

Konzistence také pomáhá porozumět. Argument dummy pointer je matoucí hack zbylý z C ++ 03. Už to není potřeba. Zvláště když potřebujete dva fiktivní ukazatele:

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

Postranní poznámka k tomuto muži. ve skutečnosti nedělá to, co chcete. Nejedná se o libovolnou referenci rvalue – jedná se o předávací referenci. Ve skutečnosti můžeme dokonce kombinovat dva konstruktory najednou:

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

Všimněte si, že to také řeší další problém s vaším kódem – totiž že nikdy nekontrolujete pokud je třída konstruovatelná kopií. Co kdybyste do své varianty chtěli vložit něco jako unique_ptr. Přesuňte konstruktivní, ale ne kopírujte konstrukční – ale nikdy jste to ve svém kódu nekontrolovali. Důležité – v opačném případě by vaši uživatelé dostávali pouze záhadné chybové zprávy.

Myslím, že tím končí část metafunkce. O něco později napíšu recenzi samotného variant.Doufám, že vám to bude užitečné.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *