Clone da Variante de Boost

Como parte da aprendizagem de C ++, com ênfase especial em C ++ 11, eu queria implementar o equivalente da Variante de Boost (localizada aqui ). Meu código está disponível em variant.hpp , com a versão atual fornecida abaixo .

Como std::aligned_storage pode ser usado portavelmente? Minha solução atual provavelmente faz uso não portátil de static_cast, no entanto se for portátil, essa informação seria muito valiosa. O código específico é semelhante a *static_cast<T*>(static_cast<void*>(&value)), para value do tipo typename std::aligned_storage<...>::type (onde ... não se destina a indicar modelos variados).

Eu faço algum uso de static_assert. Neste uso particular, SFINAE seria melhor? Eu entendo que SFINAE pode ser usado para eliminar sobrecargas do conjunto de funções viáveis, mas onde eu uso static_assert I assumir lá seria apenas uma função viável, embora eu ache valiosos exemplos de casos em que há mais de uma função viável.

Eu fiz muito uso de std::forward. É possível sobreviver com menos utilizações?

Eu fiz uso de std::enable_if em uma das sobrecargas do construtor para garantir que seria usado apenas durante uma mudança destina-se (consulte variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)). Sem ambos os enable_if s, este construtor seria usado quando o construtor de cópia variant(variant const&) fosse pretendido, mesmo que o primeiro resulte em um eventual erro do compilador. Existe uma maneira melhor de forçar esse comportamento? Uma solução que tentei foi incluir variant(variant&) como uma sobrecarga que apenas delgava para variant(variant const& rhs) – seria selecionado em variant(U&&), enquanto variant(U&&) é preferível a variant(variant const&) pelas regras de sobrecarga. Qual é a prática recomendada geral ao usar T&& para alguns T recém-introduzidos quando se pretende mover semântica, em vez de uma referência universal?

Ainda preciso adicionar multivisitores, embora esteja tendo alguns problemas com isso no caso geral (usando modelos variadic). Algo interessante que surgiu ao implementar a classe variant foram as conversões implícitas entre variant s que envolviam apenas reorganizar os argumentos do modelo ou onde o modelo lvalue argumentos são um superconjunto dos argumentos do modelo rvalue.

Todos e quaisquer comentários, perguntas ou conselhos são muito 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 

Comentários

  • De acordo com nossa central de ajuda, este não é o site para códigos que não ‘ não fazem o que deveriam Faz. Seu código funciona ?
  • Se por ” funcionar “, você quer dizer, ” ele compila? “, ele compila com o clang ++ versão 3.3, mas não compila com g ++ versão 4.8.2 devido a uma interação entre pacotes de parâmetros e lambdas. O arquivo CMakeLists.txt seleciona clang ++ incondicionalmente. Se por ” trabalho “, você quer dizer ” é portátil? “, então não, pode não ser portátil. Isso se deve à maneira como uso std::aligned_storage. Conselhos sobre o uso portátil do type associado seriam muito úteis. Se por ” work “, você quer dizer ” satisfaz a intenção original? “, a resposta é sim, embora eu gostaria muito de conselhos estilísticos e de práticas recomendadas.
  • O uso de std::forward parece bom, não consigo ‘ ver qualquer lugar onde você não possa usá-lo. Eu prefiro static_assert, pois permite que você forneça mensagens de erro melhores. A portabilidade é potencialmente um problema, pois não há garantia de que todos os ponteiros estejam alinhados no mesmo limite (portanto, void * e T* podem ter requisitos diferentes ), mas isso seria muito incomum nos dias de hoje. Eu não ‘ não sei como o impulso contorna isso, ou se eles simplesmente o ignoram.
  • @Yuushi, primeiro, obrigado por reservar um tempo para revisar isso . Acho que eles simplesmente ignoram qualquer coisa não portátil ( boost.org/doc/libs/1_55_0/boost/variant/detail/cast_storage.hpp ). Encontrei uma solução portátil ( github.com/sonyandy/cpp-experiments/blob/master/include/wart/… ) que usa uma técnica semelhante à que std::tuple usa, mas com union.
  • Sem problemas.A única outra coisa que eu ‘ d adiciono é que o arquivo parece um pouco ” ocupado ” e há coisas nele que são potencialmente úteis por si mesmas (características que você ‘ definiu como is_movable e enable_if_movable, por exemplo) que poderia potencialmente viver em outro lugar. Infelizmente, o número de revisores de C ++ por aqui com o conhecimento necessário para fornecer um bom feedback sobre isso está provavelmente na casa dos dígitos, pois o código é bastante complexo.

Resposta

Há muitas coisas aqui, então vou dividir minha revisão em partes. Quero começar focando apenas na seção de metafunções. As metafunções podem ser curtas, mas são muito poderosas e importantes para acertar – mas em termos de correção e utilidade.

Para começar:

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

O primeiro está simplesmente errado. Você está usando esta metafunção para verificar se um tipo pode ser movido (em move_construct) … mas você está fazendo isso apenas verificando se não é uma referência de lvalue nem const. Você não está realmente verificando nada relacionado à construção de movimento. Só porque algo é uma referência de rvalue não significa que você pode mover a partir dele. E só porque algo é uma referência de lvalue não significa que você não pode. Considere duas classes simples:

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

Como o nome sugere, No não pode ser movido. Sua metafunção diz que sim. Além disso, Yes& pode ser movido, mas sua metafunção diz não.

A implementação correta seria simplesmente usar o traço de tipo padrão std::is_move_constructible .

Em segundo lugar, o alias lá é questionável. Normalmente, usamos aliases para evitar ter que escreva o typename ::type cruft. Você não está fazendo isso, e a ligação resultante não é muito mais 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 

Eu pessoalmente prefiro a última versão. Observe que estou usando o alias C ++ 14 aqui. Se você não tem um compilador C ++ 14, vale a pena iniciar sua biblioteca de metafunções com todos eles. Eles são simplesmente para escrever:

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

Seguindo para:

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

Não há maneira que qualquer um saberá o que elem faz aqui. Não sabia até ler a implementação. Um nome muito melhor para isso seria contains. Mas voltarei à implementação em um momento.

Primeiro, vamos começar com:

template <bool... List> struct all; 

all é muito útil. Assim como seus parentes próximos any e none. A maneira como você escreveu all está bem e funciona, mas não faz é mais fácil escrever os outros dois. Uma boa maneira de escrever isso é usar @Columbo “s bool_pack truque:

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

Isso” s apenas seu ajudante. Você pode usar isso para implementar todo o resto facilmente:

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 assim que tivermos isso, podemos reimplementar contains como uma linha única:

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

Da mesma forma que antes, não vejo o valor em enable_if_elem . E common_result_of deve assumir o tipo , não apenas produzir a metafunção:

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

Embora seja mais fácil de colocar apenas no seu variant em si:

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

Agora vamos ao uso . Durante todo o tempo, você usa essas metafunções no tipo de retorno:

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

Ou como um ponteiro fictício:

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

Mas, em ambos os casos, acho muito mais fácil analisar expressões de modelo complexas se você colocar a lógica SFINAE como um parâmetro de modelo final sem 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); 

A consistência também ajuda a compreensão. O argumento do ponteiro fictício é um hack confuso que sobrou do C ++ 03. Não há mais necessidade disso. Especialmente quando você precisa de dois ponteiros falsos:

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

Observação lateral sobre esse cara. não faz realmente o que você quer. Esta não é uma referência de rvalue arbitrária – é uma referência de encaminhamento. Na verdade, podemos até combinar os dois construtores aqui de uma 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)); } 

Observe que isso também resolve outro problema com seu código – ou seja, você nunca verifica se uma classe é construtível por cópia. E se você quisesse colocar algo como unique_ptr em sua variante. Mover construtível, mas não construtível por cópia – mas você nunca verificou isso em seu código. Importante – caso contrário, seus usuários receberiam apenas mensagens de erro enigmáticas.

Acho que isso conclui a parte da metafunção. Escreverei uma resenha sobre o próprio variant um pouco mais tarde.Espero que você ache isto útil.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *