Clone of Boost Variant (Svenska)

Som en del av att lära mig C ++, med särskild tonvikt på C ++ 11, ville jag implementera motsvarigheten till Boosts Variant (ligger här ). Min kod finns på variant.hpp , med den aktuella versionen nedan .

Hur kan std::aligned_storage användas bärbart? Min nuvarande lösning använder förmodligen icke-bärbar användning av static_cast om den är bärbar skulle den informationen vara mycket värdefull. Den specifika koden liknar *static_cast<T*>(static_cast<void*>(&value)), för value av typen typename std::aligned_storage<...>::type (där ... inte är tänkt att indikera variabla mallar).

Jag använder en del av static_assert. Skulle SFINAE vara bättre i den här användningen? Jag förstår att SFINAE kan användas för att beskära överbelastningar från uppsättningen livskraftiga funktioner, men där jag använder static_assert I anta det skulle bara vara en livskraftig funktion, även om jag skulle hitta värdefulla några exempel på fall där det finns mer än en livskraftig funktion.

Jag använde std::forward mycket. Är det möjligt att klara sig med färre användningsområden?

Jag använde std::enable_if på en av konstruktörens överbelastningar för att säkerställa att den bara skulle användas när ett drag är avsedd (se variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Utan båda enable_if s, skulle denna konstruktör användas när kopiekonstruktören variant(variant const&) istället är avsedd, även om den förstnämnda resulterar i en eventuellt kompilatorfel. Finns det ett bättre sätt att tvinga detta beteende? En lösning jag försökte inkluderade variant(variant&) som en överbelastning som bara övergår till variant(variant const& rhs) – den skulle väljas över variant(U&&), medan variant(U&&) föredras framför variant(variant const&) av överbelastningsreglerna. Vad är den allmänna bästa praxis när du använder T&& för vissa nyligen introducerade T när flytta semantik, istället för en universell referens, är avsedda? / p>

Jag behöver fortfarande lägga till multivisatorer, även om jag har problem med detta i allmänhet (med variabla mallar). Något intressant som kom upp när klassen variant implementerades var implicita konverteringar mellan variant som bara innebar att ordna om mallargumenten eller där lvalue-mallen argument är ett superset av rvalue-mallargumenten.

Alla kommentarer, frågor eller råd är mycket uppskattade.

#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 

Kommentarer

  • Även i vårt hjälpcenter är detta inte webbplatsen för kod som inte ’ gör vad den ska do. Fungerar din kod ?
  • Om du med ” ” menar du, ” kompilerar den? ” kompilerar den med clang ++ version 3.3, men kompilerar inte med g ++ version 4.8.2 på grund av en interaktion mellan parameterpaket och lambdas. CMakeLists.txt-filen väljer clang ++ villkorslöst. Om du med ” arbetar ”, menar du ” är den bärbar? ”, då nej, det kanske inte är bärbart. Detta beror på hur jag använder std::aligned_storage. Råd om bärbar användning av tillhörande type skulle vara till stor hjälp. Om du med ” arbetar ” menar du ” uppfyller det den ursprungliga avsikten? ”, svaret är ja, men jag skulle mycket vilja ha stilistiska och bästa metoder.
  • Användningen av std::forward ser bra ut, jag kan ’ inte se någon plats där du inte kunde använda den. Jag föredrar static_assert eftersom det låter dig ge bättre felmeddelanden. Bärbarhet är potentiellt ett problem, eftersom det inte finns någon garanti att alla pekare är inriktade på samma gräns (så att void * och T* kan ha olika krav ), men det skulle vara mycket ovanligt idag. Jag vet inte ’ hur boost kommer runt detta, eller om de bara ignorerar det.
  • @Yuushi, först, tack för att du tog dig tid att granska detta . Jag tror att de bara ignorerar alla icke-bärbara saker ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Jag hittade en bärbar lösning ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ) som använder en teknik som liknar vad std::tuple använder, men med union.
  • Inga problem.Den enda andra saken som jag ’ lägger till är att filen verkar lite ” upptagen ” och det finns saker i det som är potentiellt användbara i sig (egenskaper som du ’ har definierat som is_movable och enable_if_movable, till exempel) som potentiellt kan bo någon annanstans. Tyvärr finns det antagligen antalet C ++ -granskare här med den kunskap som behövs för att ge dig bra feedback på detta i enkelsiffrorna, eftersom koden är ganska komplex.

Svar

Det finns mycket här, så jag ska dela min recension i bitar. Jag vill börja med att bara fokusera på metafunktionssektionen. Metafunktioner kan vara korta, men de är väldigt kraftfulla och viktiga för att få rätt – men när det gäller riktighet och användbarhet.

Till att börja med:

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

Den första är helt enkelt fel. Du använder den här metafunktionen för att kontrollera om en typ är flyttbar (i move_construct) … men du gör det här genom att bara kontrollera om det varken är en lvalue-referens eller const. Du kontrollerar faktiskt inte något som rör rörelsekonstruktion. Bara för att något är en värderingsreferens betyder det inte att du kan flytta från det. Och bara för att något är en referens för värdet betyder det inte att du inte kan. Tänk på två enkla klasser:

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

Som namnet antyder är No inte rörligt konstruktivt. Din metafunktion säger att den är. Dessutom är Yes& är flyttbart konstruktivt men din metafunktion säger nej.

Den rätta implementeringen skulle vara att helt enkelt använda standardtypegenskap std::is_move_constructible .

För det andra kan aliaset ifrågasättas. Normalt skulle vi använda alias för att undvika att behöva skriv typename ::type cruft. Du gör inte det, och det resulterande samtalet är inte så mycket mer kortfattat. Jämför:

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 

Jag föredrar den senaste versionen personligen. Observera att jag använder C ++ 14-aliaset här. Om du inte har en C ++ 14-kompilator är det absolut värt att starta ditt metafunktionsbibliotek med dem alla. De är helt enkelt att skriva:

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

Flytta till:

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

Det finns ingen sätt att någon vet vad elem gör här. Jag gjorde inte förrän jag läste implementeringen. Ett mycket bättre namn för detta skulle vara contains. Men jag kommer tillbaka till implementeringen omedelbart.

Låt oss först börja med:

template <bool... List> struct all; 

all är mycket användbart. Så är dess nära släktingar any och none. Sättet du skrev all är bra och fungerar, men fungerar inte det är lättare att skriva de andra två. Ett bra sätt att skriva ut dessa är att använda @Columbo ”s bool_pack trick:

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 bara din hjälpare. Du kan använda det för att enkelt implementera resten:

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

Och när vi väl har det kan vi implementera contains som en liner:

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

På samma sätt som tidigare ser jag inte värdet i enable_if_elem Och common_result_of ska ta typen , inte bara ge metafunktionen:

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

Även om det är mer lättläst att bara hålla det i din variant själv:

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

Nu till användningen . Genomgående använder du dessa metafunktioner i returtypen:

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

Eller som en dummy-pekare:

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

Men i båda fallen tycker jag att det är mycket lättare att analysera komplexa malluttryck om du sätter SFINAE-logiken som en namnlös slutmallparameter:

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

Konsistensen hjälper också till att förstå. Dummy pointer-argumentet är en förvirrande hackrester från C ++ 03. Det finns inget behov av det längre. Speciellt när du behöver två dummy-pekare:

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

Sidanot till den här killen. Det gör faktiskt inte vad du vill. Det här är inte en godtycklig referens – det är en vidarebefordringsreferens. Faktum är att vi till och med kan kombinera de två konstruktörerna här på en gång:

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

Observera att detta också löser ett annat problem med din kod – nämligen att du aldrig kontrollerar om en klass är kopierbar. Vad händer om du vill sticka något som unique_ptr i din variant. Flytta konstruktivt, men kopiera inte konstruktivt – men du aldrig kontrollerade det i din kod. Viktigt – annars skulle dina användare bara få kryptiska felmeddelanden.

Jag tror att detta avslutar metafunktionsdelen. Jag kommer att skriva en recension av variant själv lite senare.Hoppas du tycker att det här är till hjälp.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *