Clone of Boost Variant (Italiano)

Come parte dellapprendimento di C ++, con particolare enfasi su C ++ 11, volevo implementare lequivalente di Boost “s Variant (situato qui ). Il mio codice è disponibile su variant.hpp , con la versione corrente indicata di seguito .

Come può std::aligned_storage essere utilizzato in modo portatile? La mia soluzione attuale probabilmente fa un uso non portabile di static_cast, però se è portabile, tali informazioni sarebbero molto preziose. Il codice specifico è simile a *static_cast<T*>(static_cast<void*>(&value)), per value di tipo typename std::aligned_storage<...>::type (dove ... non intende indicare modelli variadici).

Faccio uso di static_assert. In questo particolare utilizzo, SFINAE sarebbe migliore? Capisco che SFINAE possa essere utilizzato per eliminare i sovraccarichi dallinsieme di funzioni valide, ma dove uso static_assert I assumere lì sarebbe solo una funzione valida, sebbene troverei utile qualsiasi esempio di casi in cui è presente più di una funzione valida.

Ho fatto molto uso di std::forward. È possibile cavarsela con meno utilizzi?

Ho utilizzato std::enable_if su uno dei sovraccarichi del costruttore per assicurarmi che sarebbe stato usato solo quando uno spostamento è inteso (vedere variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Senza entrambi i enable_if, questo costruttore verrebbe utilizzato quando il costruttore di copia variant(variant const&) è invece inteso, anche se il primo risulta in un eventuale errore del compilatore. Cè un modo migliore per forzare questo comportamento? Una soluzione che ho provato è stata includere variant(variant&) come un sovraccarico che si limita a delegare a variant(variant const& rhs) – sarebbe stato selezionato su variant(U&&), mentre variant(U&&) è preferito a variant(variant const&) dalle regole di sovraccarico. Qual è la best practice generale quando si utilizza T&& per alcuni T di recente introduzione quando si intende spostare la semantica invece di un riferimento universale?

Ho ancora bisogno di aggiungere multivisitors, anche se ho qualche problema con questo nel caso generale (usando modelli variadic). Qualcosa di interessante che è emerso durante limplementazione della classe variant sono state le conversioni implicite tra variant che coinvolgevano solo la riorganizzazione degli argomenti del modello o dove il modello lvalue gli argomenti sono un sovrainsieme degli argomenti del modello rvalue.

Qualsiasi commento, domanda o consiglio è molto apprezzato.

#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 

Commenti

  • Sempre secondo il nostro Centro assistenza, questo non è il sito per il codice che ‘ non fa ciò che dovrebbe fare. Il tuo codice funziona ?
  • Se ” funziona “, intendi: ” si compila? “, si compila con clang ++ versione 3.3, ma non si compila con g ++ versione 4.8.2 a causa di uninterazione tra pacchetti di parametri e lambda. Il file CMakeLists.txt seleziona clang ++ incondizionatamente. Se ” funziona “, intendi ” è portatile? “, quindi no, potrebbe non essere portabile. Ciò è dovuto al modo in cui utilizzo std::aligned_storage. Sarebbero molto utili consigli sulluso portatile del type associato. Se ” funziona “, intendi ” soddisfa lintento originale? “, la risposta è sì, anche se mi piacerebbe molto i consigli stilistici e di best practice.
  • Luso di std::forward sembra a posto, ‘ non vedo nessun posto dove non potresti usarlo. Preferisco static_assert in quanto ti consente di fornire messaggi di errore migliori. La portabilità è potenzialmente un problema, poiché non vi è alcuna garanzia che tutti i puntatori siano allineati sullo stesso confine (quindi void * e T* potrebbero avere requisiti diversi ), ma questo sarebbe molto insolito in questi giorni. Non ‘ non so come aggirare il boost o se semplicemente lo ignorano.
  • @Yuushi, innanzitutto, grazie per aver dedicato del tempo a rivedere questo . Penso che ignorino semplicemente qualsiasi cosa non portatile ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Ho trovato una soluzione portatile ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ) che utilizza una tecnica simile a quella utilizzata da std::tuple, ma con union.
  • Nessun problema.Lunica altra cosa che ‘ d aggiungere è che il file sembra un po ” occupato ” e ci sono cose in esso che sono potenzialmente utili di per sé (tratti che ‘ hai definito come is_movable e enable_if_movable, ad esempio) che potrebbe potenzialmente vivere altrove. Sfortunatamente, il numero di revisori C ++ qui intorno con le conoscenze necessarie per darti un buon feedback su questo è probabilmente nelle singole cifre, poiché il codice è abbastanza complesso.

Risposta

Ci sono molte cose qui, quindi dividerò la mia recensione in più parti. Voglio iniziare concentrandomi solo sulla sezione delle metafunzioni. Le meta-funzioni possono essere brevi, ma “sono molto potenti e importanti da ottenere, ma in termini di correttezza e utilità.

Per iniziare:

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

La prima è semplicemente sbagliata. Stai usando questa metafunzione per verificare se un tipo può essere spostato (in move_construct) … ma lo stai facendo controllando semplicemente se non è né un riferimento lvalue né const. In realtà non stai controllando nulla relativo alla costruzione del movimento. Solo perché qualcosa è un riferimento rvalue non significa che puoi spostarti da esso. E solo perché qualcosa è un riferimento lvalue non significa che non puoi. Considera due semplici classi:

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

Come suggerisce il nome, No non può essere spostato. La tua metafunzione dice che lo è. Inoltre, Yes& può essere spostato ma la tua metafunzione dice no.

Limplementazione corretta sarebbe semplicemente usare il tipo standard trait std::is_move_constructible .

In secondo luogo, lalias è discutibile. In genere, utilizziamo alias per evitare di dover scrivi il typename ::type cruft. Non lo stai facendo e la chiamata risultante non è molto più concisa. Confronta:

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 

Preferirei personalmente lultima versione. Nota che qui sto usando lalias C ++ 14. Se non hai un compilatore C ++ 14, vale assolutamente la pena avviare la tua libreria di metafunzioni con tutti loro. Devono semplicemente scrivere:

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

Passando a:

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

Non cè in modo che chiunque sappia cosa fa elem qui. Non lho fatto fino a quando non ho letto limplementazione. Un nome molto migliore per questo sarebbe contains. Ma tornerò allimplementazione tra un momento.

Innanzitutto, iniziamo con:

template <bool... List> struct all; 

all è molto utile. Così sono i suoi parenti stretti any e none. Il modo in cui hai scritto all va bene e funziona, ma “non fa” è più facile scrivere gli altri due. Un buon modo per scriverli è usare il trucco di @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>>; 

Quello” s solo il tuo aiuto. Puoi usarlo per implementare facilmente tutto il resto:

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

E una volta che lo abbiamo, possiamo reimplementare contains come una battuta:

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

Analogamente a prima, non vedo il valore in enable_if_elem . E common_result_of dovrebbe prendere il tipo , non solo fornire la metafunzione:

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

Anche se è più leggibile inserirlo nel tuo variant stesso:

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

Ora sulluso . In tutto, utilizzi queste metafunzioni nel tipo restituito:

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

O come puntatore fittizio:

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

Ma in entrambi i casi, trovo molto più semplice analizzare espressioni di modelli complesse se metti la logica SFINAE come parametro finale del modello senza nome:

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

La coerenza aiuta anche a capire. Largomento del puntatore fittizio è un confuso trucco rimanente da C ++ 03. Non ce nè più bisogno. Soprattutto quando hai bisogno di due puntatori fittizi:

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

Nota a margine di questo ragazzo. in realtà non fa quello che vuoi. Questo non è un riferimento arbitrario rvalue – è un riferimento di inoltro. In effetti, possiamo anche combinare i due costruttori qui in una volta sola:

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

Nota che questo risolve anche un altro problema con il tuo codice – vale a dire che non controlli mai se una classe è copia costruibile. E se volessi inserire qualcosa come unique_ptr nella tua variante. Sposta costruibile, ma non copia costruibile, ma mai lo hai verificato nel codice. Importante, altrimenti i tuoi utenti riceverebbero messaggi di errore criptici.

Penso che questo concluda la parte della metafunzione. Scriverò una recensione su variant più tardi.Spero che ti sia daiuto.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *