W ramach nauki języka C ++, ze szczególnym naciskiem na C ++ 11, chciałem zaimplementować odpowiednik wariantu Boosta (zlokalizowany tutaj ). Mój kod jest dostępny pod adresem variant.hpp , z aktualną wersją podaną poniżej .
 Jak std::aligned_storage może być używane przenośnie? Moje obecne rozwiązanie prawdopodobnie wykorzystuje static_cast jeśli jest przenośny, ta informacja byłaby bardzo cenna. Konkretny kod jest podobny do kodu *static_cast<T*>(static_cast<void*>(&value)), dla value typu typename std::aligned_storage<...>::type (gdzie ... nie ma wskazywać na szablony odmian). 
 Używam w pewnym stopniu static_assert. Czy w tym konkretnym zastosowaniu SFINAE byłaby lepsza? Rozumiem, że SFINAE może służyć do usuwania przeciążeń z zestawu wykonalnych funkcji, ale gdzie używam static_assert I Załóżmy tam byłaby tylko jedną wykonalną funkcją, chociaż uważam, że cenne są wszystkie przykłady przypadków, w których istnieje więcej niż jedna wykonalna funkcja. 
 Dużo korzystałem z std::forward. Czy można sobie poradzić z mniejszą liczbą zastosowań? 
 Użyłem std::enable_if na jednym z przeciążeń konstruktora, aby upewnić się, że będzie używany tylko podczas ruchu jest przeznaczony (patrz variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Bez obu enable_if s, ten konstruktor byłby używany, gdyby zamiast tego zamierzano konstruktor kopiujący variant(variant const&), mimo że poprzedni skutkował ewentualny błąd kompilatora. Czy istnieje lepszy sposób na wymuszenie takiego zachowania? Jednym z rozwiązań, które wypróbowałem, było uwzględnienie variant(variant&) jako przeciążenia, które po prostu przenosi się do variant(variant const& rhs) – zostanie wybrane na variant(U&&), podczas gdy variant(U&&) jest preferowane względem variant(variant const&) przez reguły przeciążenia. Jaka jest ogólna najlepsza praktyka podczas korzystania z T&& dla niektórych nowo wprowadzonych T, gdy zamiast uniwersalnego odniesienia ma być używana semantyka ruchu? 
 Nadal muszę dodawać multivisitors, chociaż mam z tym pewne problemy w przypadku ogólnym (używając szablonów wariadycznych). Coś interesującego, co pojawiło się podczas implementacji klasy variant, to niejawne konwersje między variant, które obejmowały tylko zmianę kolejności argumentów szablonu lub szablon l-wartości argumenty są nadzbiorem argumentów szablonu rvalue. 
Wszelkie komentarze, pytania lub porady są bardzo mile widziane.
#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 
Komentarze
Odpowiedź
Jest tu dużo rzeczy, więc podzielę moją recenzję na części. Chcę zacząć od skupienia się na sekcji metafunkcji. Metafunkcje mogą być krótkie, ale „są bardzo potężne i ważne, aby je wykonać – ale pod względem poprawności i użyteczności.
Na począ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>; 
 Pierwsza jest po prostu błędna. Używasz tej metafunkcji, aby sprawdzić, czy typ można skonstruować jako ruch (w move_construct) … ale robisz to teraz po prostu sprawdzając, czy nie jest to ani odwołanie do l-wartości, ani const. W rzeczywistości nie sprawdzasz niczego związanego z konstrukcją przeniesienia. Tylko dlatego, że coś jest odniesieniem do wartości r, nie oznacza, że możesz się z tego przenieść. A tylko dlatego, że coś jest odniesieniem do lwartości, nie oznacza, że nie możesz. Rozważ dwie proste klasy: 
struct No { A(A&& ) = delete; }; struct Yes { }; 
 Jak sama nazwa wskazuje, No nie można skonstruować do przenoszenia. Według Twojej metafunkcji tak jest. Ponadto Yes& można skonstruować ruch, ale twoja metafunkcja mówi nie. 
 Prawidłową implementacją byłoby po prostu użycie standardowej cechy typu  std::is_move_constructible . 
 Po drugie, alias jest wątpliwy. Zazwyczaj używamy aliasów, aby  uniknąć  konieczności napisz typename ::type cruft. Nie robisz tego, a wynikowe wezwanie nie jest o wiele bardziej zwięzłe. Porównaj: 
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 
Osobiście wolałbym ostatnią wersję. Zauważ, że używam tutaj aliasu C ++ 14. Jeśli nie masz kompilatora C ++ 14, absolutnie warto rozpocząć z nimi całą bibliotekę metafunkcji. Wystarczy napisać:
template <bool B, typename T = void> using enable_if_t = typename std::enable_if<B, T>::type; 
Przechodząc do:
template <typename Elem, typename... List> struct elem; 
 Nie ma sposób, aby każdy wiedział, co robi tutaj elem. Nie wiedziałem, dopóki nie przeczytałem implementacji. O wiele lepsza nazwa to contains. Ale za chwilę wrócę do implementacji. 
Po pierwsze, zacznijmy od:
template <bool... List> struct all; 
 all jest bardzo przydatne. Podobnie jak jego bliscy krewni any i none. Sposób, w jaki napisałeś all, jest w porządku i działa, ale nie sprawia, że łatwiej napisać pozostałe dwa. Dobrym sposobem na zapisanie tych informacji jest użycie sztuczki @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” s tylko twój pomocnik. Możesz go użyć do łatwej implementacji całej reszty:
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 kiedy już to zrobimy, możemy ponownie zaimplementować contains jako jednowierszowy: 
template <typename Elem, typename... List> using contains = any_of<std::is_same<Elem, List>::value...>; 
 Podobnie jak wcześniej, nie widzę wartości w enable_if_elem . I common_result_of powinien przyjmować  typ , a nie tylko dawać metafunkcję: 
template <typename F, typename... ArgTypes> using common_result_of = std::common_type_t<std::result_of_t<F(ArgTypes)>::...>; 
 Chociaż łatwiej jest po prostu umieścić to w swoim variant samym: 
// 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)>...>; 
Przejdźmy teraz do użycia . Przez cały czas używasz tych metafunkcji w zwracanym typie:
template <typename T> typename enable_if_movable<T>::type operator()(T&& value); 
Lub jako fikcyjny wskaźnik:
template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr) 
Ale wydaje mi się, że w obu przypadkach dużo łatwiej jest przeanalizować złożone wyrażenia szablonowe, jeśli umieścisz logikę SFINAE jako nienazwany końcowy parametr szablonu:
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); 
Spójność również pomaga w zrozumieniu. Fikcyjny argument wskaźnika to myląca pozostałość po hakowaniu z C ++ 03. Nie ma już takiej potrzeby. Zwłaszcza gdy potrzebujesz dwóch fałszywych wskaźników:
 template <typename U, typename = std::enable_if_t<contains<U, T...>::value && std::is_move_constructible<U>::value> > variant(U&& value); 
Dodatkowa uwaga na temat tego gościa. właściwie nie robi tego, co chcesz. To nie jest arbitralne odniesienie do wartości r – jest to odniesienie przekazujące. W rzeczywistości możemy tutaj nawet połączyć oba konstruktory za jednym razem:
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)); } 
 Zauważ, że rozwiązuje to również inny problem z twoim kodem – mianowicie, że nigdy nie sprawdzasz jeśli klasa jest możliwa do skopiowania. Co by było, gdybyś chciał umieścić w swoim wariancie coś takiego jak unique_ptr. Przenieś konstruowalne, ale nie kopiuj konstruktywne – ale  nigdy  tego nie sprawdziłeś w swoim kodzie. Ważne – w przeciwnym razie twoi użytkownicy otrzymaliby po prostu tajemnicze komunikaty o błędach. 
 Myślę, że na tym kończy się część dotycząca metafunkcji. Recenzję samego variant napiszę nieco później.Mam nadzieję, że okaże się to pomocne. 
std::aligned_storage. Rady dotyczące przenośnego użycia powiązanegotypebyłyby bardzo pomocne. Jeśli ” działa „, oznacza to, że ” spełnia pierwotne zamierzenie? „, odpowiedź brzmi tak, chociaż bardzo chciałbym uzyskać porady dotyczące stylu i najlepszych praktyk.std::forwardwygląda dobrze, nie mogę ' zobaczyć żadnego miejsca, w którym nie mógłbyś go użyć. Preferujęstatic_assert, ponieważ umożliwia to wyświetlanie lepszych komunikatów o błędach. Przenośność jest potencjalnie problemem, ponieważ nie ma gwarancji, że wszystkie wskaźniki są wyrównane na tej samej granicy (więcvoid *iT*mogą mieć różne wymagania ), ale w dzisiejszych czasach byłoby to bardzo niezwykłe. Nie ' nie wiem, jak sobie z tym radzi przyspieszenie, czy po prostu je ignorują.std::tuple, ale zunion.is_movableienable_if_movable, na przykład), które mogłyby potencjalnie mieszkać gdzie indziej. Niestety, liczba recenzentów C ++ z wiedzą potrzebną do udzielenia dobrych opinii na ten temat jest prawdopodobnie jednocyfrowa, ponieważ kod jest dość złożony.