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ązanegotype
był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::forward
wyglą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_movable
ienable_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.