Recentemente, acompanhei uma discussão no Reddit que levou a uma boa comparação de std::visit
otimização entre compiladores. Notei o seguinte: https://godbolt.org/z/D2Q5ED
O GCC9 e o Clang9 (acho que compartilham o mesmo stdlib) não geram código para verificar e lançar uma exceção sem valor quando todos os tipos atendem a algumas condições. Isso leva a um codegen muito melhor; portanto, levantei um problema com o MSVC STL e fui apresentado com este código:
template <class T>
struct valueless_hack {
struct tag {};
operator T() const { throw tag{}; }
};
template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
try { v.emplace<0>(valueless_hack<First>()); }
catch(typename valueless_hack<First>::tag const&) {}
}
A alegação era de que isso torna qualquer variante sem valor e, lendo o documento , deveria:
Primeiro, destrói o valor atualmente contido (se houver). Inicializa diretamente o valor contido como se estivesse construindo um valor do tipo
T_I
com os argumentosstd::forward<Args>(args)....
Se uma exceção for lançada,*this
pode se tornar valueless_by_exception.
O que não entendo: por que é indicado como "pode"? É legal permanecer no estado antigo se toda a operação for lançada? Porque é isso que o GCC faz:
// For suitably-small, trivially copyable types we can create temporaries
// on the stack and then memcpy them into place.
template<typename _Tp>
struct _Never_valueless_alt
: __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
{ };
E mais tarde (condicionalmente) faz algo como:
T tmp = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);
Portanto, basicamente ele cria um temporário e, se isso for bem sucedido, o copia / move para o lugar real.
Na IMO, esta é uma violação de "Primeiro, destrói o valor atualmente contido", conforme declarado pelo documento. Enquanto leio o padrão, depois de um v.emplace(...)
valor atual na variante é sempre destruído e o novo tipo é o tipo definido ou sem valor.
Eu entendo que a condição is_trivially_copyable
exclui todos os tipos que têm um destruidor observável. Portanto, isso também pode ser feito como: "a variante como se é reinicializada com o valor antigo" ou mais. Mas o estado da variante é um efeito observável. O padrão permite, de fato, queemplace
isso não mude o valor atual?
Edite em resposta a uma cotação padrão:
Em seguida, inicializa o valor contido como se estivesse iniciando diretamente um valor do tipo TI com os argumentos
std::forward<Args>(args)...
.
Será que T tmp {std::forward<Args>(args)...}; this->value = std::move(tmp);
realmente contam como uma implementação válida do acima? É isso que se entende por "como se"?
fonte
might/may
redação, pois o padrão não indica qual é a alternativa.there is no way to detect the difference
.Sim.
emplace
deve fornecer a garantia básica de nenhum vazamento (isto é, respeitar a vida útil do objeto quando a construção e a destruição produzem efeitos colaterais observáveis), mas, quando possível, é permitido fornecer a garantia forte (isto é, o estado original é mantido quando uma operação falha).variant
deve se comportar de maneira semelhante a uma união - as alternativas são alocadas em uma região de armazenamento alocado adequadamente. Não é permitido alocar memória dinâmica. Portanto, uma alteração de tipoemplace
não tem como manter o objeto original sem chamar um construtor de movimento adicional - precisa destruí-lo e construir o novo objeto no lugar dele. Se essa construção falhar, a variante deverá passar ao estado excepcional sem valor. Isso evita coisas estranhas, como destruir um objeto inexistente.No entanto, para pequenos tipos trivialmente copiáveis, é possível fornecer uma garantia forte sem muita sobrecarga (até mesmo um aumento de desempenho para evitar uma verificação, neste caso). Portanto, a implementação faz isso. Isso está em conformidade com o padrão: a implementação ainda fornece a garantia básica conforme exigido pelo padrão, apenas de uma maneira mais amigável.
Sim, se a atribuição de movimentação não produzir efeito observável, é o caso de tipos trivialmente copiáveis.
fonte
std::variant
não há motivos para quebrar isso. Concordo que isso pode ser mais explícito na redação do padrão, mas é basicamente assim que outros fazem parte da biblioteca padrão. E, para sua informação, P0088 foi a proposta inicial.if an exception is thrown during the call toT’s constructor, valid()will be false;
Assim que fez proibir este "otimização"emplace
em P0088 emException safety