Dans le cadre de lapprentissage du C ++, avec un accent particulier sur C ++ 11, jai voulu implémenter léquivalent du Variant de Boost (situé ici ). Mon code est disponible à variant.hpp , avec la version actuelle indiquée ci-dessous .
Comment std::aligned_storage
peut-il être utilisé de manière portative? Ma solution actuelle fait probablement une utilisation non portable de static_cast
, cependant si elle est portable, cette information serait très précieuse. Le code particulier est similaire à *static_cast<T*>(static_cast<void*>(&value))
, pour value
de type typename std::aligned_storage<...>::type
(où ...
nest pas censé indiquer des modèles variadiques).
Jutilise static_assert
. Dans cette utilisation particulière, SFINAE serait-il meilleur? Je comprends que SFINAE peut être utilisé pour éliminer les surcharges de lensemble des fonctions viables, mais lorsque jutilise static_assert
I assumer là ne serait quune fonction viable, même si je trouverais des exemples intéressants de cas où il y a plus dune fonction viable.
Jai beaucoup utilisé std::forward
. Est-il possible de sen sortir avec moins dutilisations?
Jai utilisé std::enable_if
sur lune des surcharges du constructeur pour massurer quil ne serait utilisé que lors dun déplacement est prévu (voir variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)
). Sans les deux enable_if
s, ce constructeur serait utilisé lorsque le constructeur de copie variant(variant const&)
est plutôt voulu, même si le premier aboutit à un erreur éventuelle du compilateur. Existe-t-il une meilleure façon de forcer ce comportement? Une solution que jai essayée était dinclure variant(variant&)
en tant que surcharge qui se délimite simplement à variant(variant const& rhs)
– il serait sélectionné sur variant(U&&)
, tandis que variant(U&&)
est préféré à variant(variant const&)
par les règles de surcharge. Quelle est la bonne pratique générale lors de lutilisation de T&&
pour certains T
nouvellement introduits lorsque la sémantique de déplacement, au lieu dune référence universelle, est prévue?
Jai encore besoin dajouter des multivisiteurs, même si jai quelques problèmes avec cela dans le cas général (en utilisant des modèles variadiques). Quelque chose dintéressant qui est apparu lors de limplémentation de la classe variant
était les conversions implicites entre variant
qui impliquaient uniquement de réorganiser les arguments du modèle ou lorsque le modèle lvalue Les arguments sont un sur-ensemble des arguments du modèle rvalue.
Tous les commentaires, questions ou conseils sont très appréciés.
#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
Commentaires
Réponse
Il y en a beaucoup ici, donc je vais diviser ma critique en morceaux. Je veux commencer par me concentrer uniquement sur la section métafonction. Les métafonctions peuvent être courtes, mais elles « sont très puissantes et importantes à bien faire – mais en termes dexactitude et dutilité.
Pour commencer:
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>;
Le premier est tout simplement faux. Vous utilisez cette métafonction pour vérifier si un type est move constructible (dans move_construct
) … mais vous le faites en vérifiant simplement si ce nest ni une référence lvalue ni const
. En fait, vous ne vérifiez rien concernant la construction de déplacement. Ce nest pas parce que quelque chose est une référence rvalue que vous pouvez en sortir. Et ce nest pas parce que quelque chose est une référence lvalue que vous ne pouvez pas. Considérez deux classes simples:
struct No { A(A&& ) = delete; }; struct Yes { };
Comme son nom lindique, No
nest pas constructible de déplacement. Votre métafonction lindique. De plus, Yes&
est une construction constructible mais votre métafonction dit non.
Limplémentation correcte serait simplement dutiliser le trait de type standard std::is_move_constructible
.
Deuxièmement, lalias y est discutable. En général, nous utiliserions des alias pour éviter davoir à écrivez le typename ::type
cruft. Vous ne faites pas cela, et lappel qui en résulte nest pas beaucoup plus concis. Comparez:
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
Personnellement, je préférerais la dernière version. Notez que jutilise ici lalias C ++ 14. Si vous navez pas de compilateur C ++ 14, cela vaut vraiment la peine de démarrer votre bibliothèque de métafonctions avec tous. Il suffit décrire:
template <bool B, typename T = void> using enable_if_t = typename std::enable_if<B, T>::type;
Passer à:
template <typename Elem, typename... List> struct elem;
Il ny a pas façon que tout le monde saura ce que elem
fait ici. Je ne lai pas fait avant davoir lu limplémentation. Un bien meilleur nom pour cela serait contains
. Mais je reviendrai à limplémentation dans un instant.
Commençons par:
template <bool... List> struct all;
all
est super utile. Ses proches le sont aussi any
et none
. La façon dont vous avez écrit all
est correcte et fonctionne, mais ne fait pas il est plus facile décrire les deux autres. Une bonne façon de les écrire est dutiliser @Columbo « s bool_pack
astuce:
template <bool...> struct bool_pack; template <bool f, bool... bs> using all_same = std::is_same<bool_pack<f, bs...>, bool_pack<bs..., f>>;
Cest » s juste votre aide. Vous pouvez lutiliser pour implémenter tout le reste facilement:
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>;
Et une fois que nous lavons, nous pouvons réimplémenter contains
en une seule ligne:
template <typename Elem, typename... List> using contains = any_of<std::is_same<Elem, List>::value...>;
Comme avant, je ne vois pas la valeur dans enable_if_elem
. Et common_result_of
devrait prendre le type , pas seulement donner la métafonction:
template <typename F, typename... ArgTypes> using common_result_of = std::common_type_t<std::result_of_t<F(ArgTypes)>::...>;
Bien quil soit plus facile de le coller dans votre variant
lui-même:
// 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)>...>;
Passons maintenant à lutilisation . Tout au long, vous utilisez ces métafonctions dans le type de retour:
template <typename T> typename enable_if_movable<T>::type operator()(T&& value);
Ou comme pointeur factice:
template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr)
Mais dans les deux cas, je trouve beaucoup plus facile danalyser des expressions de modèle complexes si vous mettez la logique SFINAE comme paramètre de modèle final sans nom:
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 cohérence aide aussi à comprendre. Largument pointeur factice est un hack déroutant laissé par C ++ 03. Il ny en a plus besoin. Surtout quand vous avez besoin de deux pointeurs factices:
template <typename U, typename = std::enable_if_t<contains<U, T...>::value && std::is_move_constructible<U>::value> > variant(U&& value);
Note sur ce type. ne fait pas vraiment ce que vous voulez. Ce nest pas une référence rvalue arbitraire – cest une référence de transfert. En fait, nous pouvons même combiner les deux constructeurs ici en une seule fois:
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)); }
Notez que cela résout également un autre problème avec votre code – à savoir que vous ne vérifiez jamais si une classe est constructible par copie. Et si vous vouliez insérer quelque chose comme unique_ptr
dans votre variante. Déplacer constructible, mais pas copier constructible – mais vous ne lavez jamais vérifié dans votre code. Important – sinon vos utilisateurs recevraient juste des messages derreur cryptiques.
Je pense que cela conclut la partie métafonction. Jécrirai un examen de variant
lui-même un peu plus tard.Jespère que ceci vous aidera.
std::aligned_storage
. Des conseils sur lutilisation portable dutype
associé seraient très utiles. Si par » travail « , vous voulez dire que » répond-il à lintention dorigine? « , la réponse est oui, même si jaimerais beaucoup des conseils stylistiques et de bonnes pratiques.std::forward
a lair bien, je ne peux ‘ voir aucun endroit où vous ne pourriez pas lutiliser. Je préfèrestatic_assert
car cela vous permet de donner de meilleurs messages derreur. La portabilité est potentiellement un problème, car il ny a aucune garantie que tous les pointeurs sont alignés sur la même limite (doncvoid *
etT*
peuvent avoir des exigences différentes ), mais ce serait très inhabituel de nos jours. Je ne ‘ ne sais pas comment le boost contourne cela, ou sils lignorent simplement.std::tuple
, mais avecunion
.is_movable
etenable_if_movable
, par exemple) qui pourraient potentiellement vivre ailleurs. Malheureusement, le nombre de réviseurs C ++ ici avec les connaissances nécessaires pour vous donner de bons commentaires à ce sujet est probablement à un chiffre, car le code est assez complexe.