Ca parte a învățării C ++, cu accent special pe C ++ 11, am vrut să implementez echivalentul Variantei Boost (localizat aici ). Codul meu este disponibil la variant.hpp , cu versiunea curentă dată mai jos .
Cum se poate utiliza std::aligned_storage
portabil? Soluția mea actuală face probabil utilizarea neportabilă a static_cast
dacă este portabil, acea informație ar fi foarte valoroasă. Codul particular este similar cu *static_cast<T*>(static_cast<void*>(&value))
, pentru value
de tip typename std::aligned_storage<...>::type
(unde ...
nu este menit să indice șabloane variadice).
Folosesc oarecum static_assert
. În această utilizare specială, SFINAE ar fi mai bun? Înțeleg că SFINAE poate fi folosit pentru a elimina suprasarcinile din setul de funcții viabile, dar unde folosesc static_assert
presupune acolo ar fi o singură funcție viabilă, deși aș găsi valoroase orice exemple de cazuri în care există mai multe funcții viabile.
Am folosit mult std::forward
. Este posibil să vă descurcați cu mai puține utilizări?
Am folosit std::enable_if
pe una dintre supraîncărcările constructorului pentru a mă asigura că va fi utilizată numai la o mutare este destinat (vezi variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)
). Fără ambele enable_if
, acest constructor ar fi utilizat atunci când se intenționează în schimb constructorul de copii variant(variant const&)
, chiar dacă primul rezultă într-un eventuala eroare a compilatorului. Există o modalitate mai bună de a forța acest comportament? O soluție pe care am încercat-o a fost aceea de a include variant(variant&)
ca o suprasarcină care tocmai delgează la variant(variant const& rhs)
– ar fi selectată peste variant(U&&)
, în timp ce variant(U&&)
este preferat în locul variant(variant const&)
de regulile de suprasarcină. Care este cea mai bună practică generală atunci când se utilizează T&&
pentru unele T
nou introduse atunci când se intenționează semantica mutării, în loc de o referință universală?
Încă trebuie să adaug multivizitori, deși am probleme cu acest lucru în cazul general (folosind șabloane variadice). Ceva interesant care a apărut la implementarea clasei variant
a fost conversiile implicite între variant
s care implicau doar rearanjarea argumentelor șablonului sau unde șablonul lvalue argumentele sunt un superset al argumentelor șablonului rvalue.
Orice comentarii, întrebări sau sfaturi sunt foarte apreciate.
#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
std::aligned_storage
. Sfaturi privind utilizarea portabilă atype
ar fi foarte utile. Dacă prin " funcționează ", vrei să spui " îndeplinește intenția inițială? ", răspunsul este da, deși aș dori foarte mult sfaturi stilistice și de bune practici.std::forward
arată bine, nu pot ' să văd niciun loc unde nu l-ai putea folosi. Preferstatic_assert
, deoarece vă permite să transmiteți mesaje de eroare mai bune. Portabilitatea este potențial o problemă, deoarece nu există nicio garanție că toți indicatorii sunt aliniați pe aceeași limită (decivoid *
șiT*
pot avea cerințe diferite ), dar acest lucru ar fi foarte neobișnuit în zilele noastre. Nu ' nu știu cum crește acest lucru sau dacă doar îl ignoră.std::tuple
, dar cuunion
.is_movable
șienable_if_movable
, de exemplu) care ar putea trăi în altă parte. Din păcate, numărul de recenzori C ++ de aici, cu cunoștințele necesare pentru a vă oferi feedback bun în acest sens, este probabil format din cifre unice, deoarece codul este destul de complex.Există „multe aici, așa că voi împărți recenzia mea în bucăți. Vreau să încep prin a mă concentra doar pe secțiunea metafuncție. Metafuncțiile pot fi scurte, dar sunt „foarte puternice și importante pentru a obține corect – dar din punct de vedere al corectitudinii și utilității.
Pentru a începe cu:
Primul este pur și simplu greșit. Folosiți această metafuncție pentru a verifica dacă un tip este mutabil construibil (în
move_construct
) … dar faceți acest lucru doar verificând dacă nu este nici o referință lvalue și niciconst
. Nu verificați de fapt nimic referitor la construcția de mutare. Doar pentru că ceva este o referință de valoare nu înseamnă că puteți trece de la aceasta. Și doar pentru că ceva este o referință de valoare nu înseamnă că nu puteți. Luați în considerare două clase simple:După cum sugerează și numele,
No
nu se poate muta construibil. Metafuncția dvs. spune că este. De asemenea,Yes&
este mutabil construibil, dar metafuncția dvs. spune că nu.Implementarea corectă ar fi să folosiți pur și simplu caracteristica de tip standard
std::is_move_constructible
.În al doilea rând, aliasul de acolo este discutabil. De obicei, am folosi aliasuri pentru a evita să fie scrieți
typename ::type
cruft. Nu faceți asta, iar apelul rezultat nu este mult mai concis. Comparați:Aș prefera ultima versiune personal. Rețineți că folosesc aliasul C ++ 14 aici. Dacă nu aveți un compilator C ++ 14, merită absolut să începeți biblioteca de metafuncții cu toate acestea. Acestea trebuie să scrie pur și simplu:
Trecând la:
Nu există mod în care oricine va ști ce face
elem
aici. Nu am făcut până am citit implementarea. Un nume mult mai bun pentru aceasta ar ficontains
. Dar voi reveni la implementare într-un moment.În primul rând, să începem cu:
all
este foarte util. La fel și rudele sale apropiateany
șinone
. Modul în care ai scrisall
este bine și funcționează, dar nu face este mai ușor să scrii pe celelalte două. O modalitate bună de scriere a acestora este de a folosi @Columbo „sbool_pack
truc:That” s doar ajutorul tău. Puteți utiliza asta pentru a implementa cu ușurință toate celelalte:
Și odată ce avem acest lucru, putem reimplementa
contains
ca un singur liner:La fel ca înainte, nu văd valoarea în
enable_if_elem
. Șicommon_result_of
ar trebui să ia tip , nu doar să dea metafuncția:Deși este mai ușor să vă lipiți doar de
variant
:Acum, folosiți . Pe tot parcursul, utilizați aceste metafuncții în tipul de returnare:
Sau ca un indicator fals:
Dar în ambele cazuri, consider că este mult mai ușor să analizez expresiile de șabloane complexe dacă puneți logica SFINAE ca parametru șablon final fără nume:
Coerența ajută și la înțelegere. Argumentul indicatorului fals este un hack confuz care a rămas din C ++ 03. Nu mai este necesar. Mai ales când aveți nevoie de doi indicatori fictivi:
Notă laterală pentru acest tip. de fapt nu face ceea ce vrei. Aceasta nu este o referință de valoare arbitrară – este o referință de redirecționare. De fapt, putem combina chiar și cei doi constructori aici dintr-o dată:
Rețineți că acest lucru rezolvă și o altă problemă cu codul dvs. – și anume că nu verificați niciodată dacă o clasă este copie construibilă. Ce se întâmplă dacă ai vrea să introduci ceva de genul
unique_ptr
în varianta ta. Mutați construibil, dar nu copiați construibil – dar niciodată nu ați verificat acest lucru în cod. Important – în caz contrar, utilizatorii dvs. ar primi doar mesaje de eroare criptice.Cred că acest lucru încheie partea metafuncției. Voi scrie o recenzie a
variant
însăși puțin mai târziu.Sper să vă fie de ajutor.