Clone of Boost Variant (Polski)

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

  • Również według naszego centrum pomocy, to nie jest witryna dla kodu, który ' nie robi tego, co powinien zrobić. Czy Twój kod działa ?
  • Jeśli przez ” działa „, masz na myśli, ” czy się kompiluje? „, kompiluje się z clang ++ w wersji 3.3, ale nie kompiluje się z g ++ w wersji 4.8.2 z powodu interakcji między pakietami parametrów a lambdami. Plik CMakeLists.txt wybiera clang ++ bezwarunkowo. Jeśli ” działa „, masz na myśli ” czy jest przenośny? „, to nie, może nie być przenośny. Wynika to ze sposobu, w jaki używam std::aligned_storage. Rady dotyczące przenośnego użycia powiązanego type 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.
  • Zastosowanie 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ęc void * i T* 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ą.
  • @Yuushi, po pierwsze, dziękuję za poświęcenie czasu na przejrzenie tego . Myślę, że po prostu ignorują wszelkie nieprzenośne rzeczy ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Znalazłem rozwiązanie przenośne ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ), która wykorzystuje technikę podobną do tej, której używa std::tuple, ale z union.
  • Żaden problem.Jedyną inną rzeczą, którą ' d dodaję, jest to, że plik wydaje się nieco ” zajęty ” i są w nim rzeczy, które są potencjalnie przydatne same w sobie (cechy, które ' zdefiniowałeś, takie jak is_movable i enable_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.

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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *