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
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é.
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.std::forward
vypadá dobře, ‚ nevidím žádné místo, kde byste jej nemohli použít. Dávám přednoststatic_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 *
aT*
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í.std::tuple
používá, ale sunion
.is_movable
aenable_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ý.