Clona Variantei Boost

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 

Comentarii

  • De asemenea, în centrul nostru de ajutor, acesta nu este site-ul pentru cod care nu ' face ceea ce ar trebui să facă do. Codul dvs. funcționează ?
  • Dacă prin " funcționează ", adică, " compilează? ", compilează cu clang ++ versiunea 3.3, dar nu compilează cu g ++ versiunea 4.8.2 din cauza unei interacțiuni între pachetele de parametri și lambdas. Fișierul CMakeLists.txt selectează necondiționat clang ++. Dacă prin " funcționează ", vrei să spui " este portabil? ", atunci nu, este posibil să nu fie portabil. Acest lucru se datorează modului în care folosesc std::aligned_storage. Sfaturi privind utilizarea portabilă a type 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.
  • Utilizarea std::forward arată bine, nu pot ' să văd niciun loc unde nu l-ai putea folosi. Prefer static_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ă (deci void * și T* 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ă.
  • @Yuushi, mai întâi, vă mulțumesc că ați luat timp pentru a examina acest lucru . Cred că ignoră orice element neportabil ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Am găsit o soluție portabilă ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ) care utilizează o tehnică similară cu ceea ce folosește std::tuple, dar cu union.
  • Nicio problemă.Singurul alt lucru pe care l-am adăugat ' este că fișierul pare puțin " ocupat " și există lucruri care sunt potențial utile în sine (trăsături pe care le ' le-ați definit ca is_movable și enable_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:

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

    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 nici const. 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:

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

    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:

    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 

    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:

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

    Trecând la:

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

    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 fi contains. Dar voi reveni la implementare într-un moment.

    În primul rând, să începem cu:

    template <bool... List> struct all; 

    all este foarte util. La fel și rudele sale apropiate any și none. Modul în care ai scris all 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 „s bool_pack truc:

    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 doar ajutorul tău. Puteți utiliza asta pentru a implementa cu ușurință toate celelalte:

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

    Și odată ce avem acest lucru, putem reimplementa contains ca un singur liner:

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

    La fel ca înainte, nu văd valoarea în enable_if_elem . Și common_result_of ar trebui să ia tip , nu doar să dea metafuncția:

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

    Deși este mai ușor să vă lipiți doar de variant:

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

    Acum, folosiți . Pe tot parcursul, utilizați aceste metafuncții în tipul de returnare:

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

    Sau ca un indicator fals:

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

    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:

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

    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:

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

    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ă:

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

    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.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *