Clon de Boost Variant

Como parte del aprendizaje de C ++, con especial énfasis en C ++ 11, quería implementar el equivalente de Boost «s Variant (ubicado aquí ). Mi código está disponible en variant.hpp , con la versión actual a continuación .

¿Cómo se puede usar std::aligned_storage de manera portátil? Mi solución actual probablemente hace un uso no portátil de static_cast, aunque si es portátil, esa información sería muy valiosa. El código en particular es similar a *static_cast<T*>(static_cast<void*>(&value)), para value de tipo typename std::aligned_storage<...>::type (donde ... no pretende indicar plantillas variadas).

Utilizo static_assert. En este uso particular, ¿SFINAE sería mejor? Entiendo que SFINAE se puede usar para eliminar las sobrecargas del conjunto de funciones viables, pero donde uso static_assert I asumir allí sería sólo una función viable, aunque encontraría valiosos ejemplos de casos en los que hay más de una función viable.

Hice mucho uso de std::forward. ¿Es posible arreglárselas con menos usos?

Hice uso de std::enable_if en una de las sobrecargas del constructor para asegurarme de que solo se usaría cuando un movimiento está previsto (consulte variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Sin ambos enable_if s, este constructor se usaría cuando el constructor de copia variant(variant const&) está previsto, aunque el primero da como resultado un eventual error del compilador. ¿Existe una mejor manera de forzar este comportamiento? Una solución que probé fue incluir variant(variant&) como una sobrecarga que simplemente se delega en variant(variant const& rhs); se seleccionaría sobre variant(U&&), mientras que variant(U&&) se prefiere sobre variant(variant const&) por las reglas de sobrecarga. ¿Cuál es la mejor práctica general cuando se usa T&& para algunos T recién introducidos cuando se pretende la semántica de movimiento, en lugar de una referencia universal?

Todavía necesito agregar multivisitores, aunque tengo algunos problemas con esto en el caso general (usando plantillas variadas). Algo interesante que surgió al implementar la clase variant fueron las conversiones implícitas entre variant s que solo implicaban reorganizar los argumentos de la plantilla o donde la plantilla lvalue Los argumentos son un superconjunto de los argumentos de la plantilla rvalue.

Todos y cada uno de los comentarios, preguntas o consejos son muy apreciados.

#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 

Comentarios

  • También según nuestro centro de ayuda, este no es el sitio para el código que no ‘ no hace lo que se supone que debe hacer hacer. ¿Su código funciona ?
  • Si por » funciona «, quiere decir, » ¿se compila? «, se compila con clang ++ versión 3.3, pero no se compila con g ++ versión 4.8.2 debido a una interacción entre paquetes de parámetros y lambdas. El archivo CMakeLists.txt selecciona clang ++ incondicionalmente. Si » funciona «, te refieres a » ¿es portátil? «, entonces no, puede que no sea portátil. Esto se debe a la forma en que utilizo std::aligned_storage. Un consejo sobre el uso portátil del type asociado sería muy útil. Si » funciona «, te refieres a » ¿satisface la intención original? «, la respuesta es sí, aunque me gustaría mucho recibir consejos de estilo y mejores prácticas.
  • El uso de std::forward se ve bien, no puedo ‘ ver ningún lugar donde no pueda usarlo. Prefiero static_assert ya que te permite dar mejores mensajes de error. La portabilidad es potencialmente un problema, ya que no hay garantía de que todos los punteros estén alineados en el mismo límite (por lo que void * y T* pueden tener requisitos diferentes ), pero esto sería muy inusual en estos días. No ‘ no sé cómo se soluciona el problema con boost, o si simplemente lo ignoran.
  • @Yuushi, primero, gracias por tomarse el tiempo para revisar esto . Creo que simplemente ignoran cualquier material no portátil ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Encontré una solución portátil ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ) que usa una técnica similar a la que usa std::tuple, pero con union.
  • No hay problema.La única otra cosa que ‘ agrego es que el archivo parece un poco » ocupado » y hay cosas en él que son potencialmente útiles por derecho propio (rasgos que ‘ has definido como is_movable y enable_if_movable, por ejemplo) que potencialmente podría vivir en otro lugar. Desafortunadamente, la cantidad de revisores de C ++ por aquí con el conocimiento necesario para brindarle una buena retroalimentación sobre esto probablemente sea de un solo dígito, ya que el código es bastante complejo.

Respuesta

Hay mucho aquí, así que voy a dividir mi reseña en partes. Quiero comenzar concentrándome en la sección de metafunción. Las metafunciones pueden ser cortas, pero son muy poderosas e importantes para hacerlas bien, pero en términos de corrección y utilidad.

Para empezar:

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

El primero es simplemente incorrecto. Estás usando esta metafunción para comprobar si un tipo se puede mover (en move_construct) … pero estás haciendo esto simplemente verificando si no es una referencia de lvalue ni const. En realidad, no está comprobando nada relacionado con la construcción de movimientos. El hecho de que algo sea una referencia de valor r no significa que pueda moverse de él. Y el hecho de que algo sea una referencia de valor l no significa que no pueda hacerlo. Considere dos clases simples:

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

Como sugiere el nombre, No no es un movimiento constructible. Tu metafunción dice que sí. Además, Yes& es move construible pero su metafunción dice que no.

La implementación correcta sería simplemente usar el rasgo de tipo estándar std::is_move_constructible .

En segundo lugar, el alias es cuestionable. Por lo general, usamos alias para evitar tener que escribe el typename ::type cruft. No estás haciendo eso y la llamada resultante no es mucho más concisa. Compare:

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 

Personalmente, preferiría la última versión. Tenga en cuenta que aquí estoy usando el alias de C ++ 14. Si no tiene un compilador de C ++ 14, vale la pena iniciar su biblioteca de metafunción con todos ellos. Simplemente deben escribir:

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

Pasando a:

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

No hay de manera que cualquiera sepa lo que elem hace aquí. No lo hice hasta que leí la implementación. Un nombre mucho mejor para esto sería contains. Pero volveré a la implementación en un momento.

Primero, comencemos con:

template <bool... List> struct all; 

all es muy útil. También lo son sus parientes cercanos any y none. La forma en que escribiste all está bien y funciona, pero no hace es más fácil escribir los otros dos. Una buena forma de escribirlos es utilizar el bool_pack truco de @Columbo:

template <bool...> struct bool_pack; template <bool f, bool... bs> using all_same = std::is_same<bool_pack<f, bs...>, bool_pack<bs..., f>>; 

Eso es solo tu ayudante. Puede usar eso para implementar todo el resto fácilmente:

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

Y una vez que lo tengamos, podemos volver a implementar contains como una sola línea:

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

De manera similar a antes, no veo el valor en enable_if_elem . Y common_result_of debería tomar el tipo , no solo generar la metafunción:

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

Aunque es más fácil de leer simplemente pegar eso en su variant sí mismo:

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

Ahora en el uso . En todo momento, utiliza estas metafunciones en el tipo de retorno:

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

O como un puntero ficticio:

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

Pero en ambos casos, me resulta mucho más fácil analizar expresiones de plantilla complejas si pones la lógica SFINAE como un parámetro de plantilla final sin nombre:

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 coherencia también ayuda a comprender. El argumento del puntero ficticio es un truco confuso sobrante de C ++ 03. Ya no es necesario. Especialmente cuando necesitas dos punteros ficticios:

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

Nota al margen sobre este tipo. Es en realidad no hace lo que quiere. Esta no es una referencia de valor arbitraria, es una referencia de reenvío. De hecho, incluso podemos combinar los dos constructores aquí de una vez:

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

Tenga en cuenta que esto también resuelve otro problema con su código, es decir, que nunca verifica si una clase es copia construible. ¿Qué pasaría si quisieras incluir algo como unique_ptr en tu variante? Mover construible, pero no copiar construible, pero nunca lo verificó en su código. Importante: de lo contrario, sus usuarios simplemente recibirían mensajes de error crípticos.

Creo que esto concluye la parte de la metafunción. Escribiré una revisión de variant un poco más tarde.Espero que le resulte útil.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *