O GCC9 está evitando o estado sem valor de std :: variant permitido?

14

Recentemente, acompanhei uma discussão no Reddit que levou a uma boa comparação de std::visitotimizaçã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_Icom os argumentos std::forward<Args>(args)....Se uma exceção for lançada, *thispode 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_copyableexclui 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"?

Fogo Flamejante
fonte

Respostas:

7

Eu acho que a parte importante do padrão é esta:

De https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Modi fi cadores

(...)

modelo variant_alternative_t> & emplace (Argumentos && ... args);

(...) Se uma exceção for lançada durante a inicialização do valor contido, a variante poderá não conter um valor

Diz "pode" não "deve". Eu esperaria que isso fosse intencional para permitir implementações como a usada pelo gcc.

Como você se mencionou, isso só é possível se os destruidores de todas as alternativas forem triviais e, portanto, não observáveis, pois é necessário destruir o valor anterior.

Questão a seguir:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

Ttmp {std :: forward (args) ...}; this-> value = std :: move (tmp); realmente conta como uma implementação válida dos itens acima? É isso que se entende por "como se"?

Sim, porque para tipos que são trivialmente copiáveis, não há como detectar a diferença; portanto, a implementação se comporta como se o valor tivesse sido inicializado conforme descrito. Isso não funcionaria se o tipo não fosse trivialmente copiável.

PaulR
fonte
Interessante. Atualizei a pergunta com uma solicitação de acompanhamento / esclarecimento. A raiz é: a cópia / movimentação é permitida? Estou muito confuso com a might/mayredação, pois o padrão não indica qual é a alternativa.
Flamefire 13/11/19
Aceitando isso para a cotação padrão e there is no way to detect the difference.
Flamefire 14/11/19
5

Então, o padrão realmente permite que emplaceisso não mude o valor atual?

Sim. emplacedeve 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).

variantdeve 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 tipo emplacenã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.

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"?

Sim, se a atribuição de movimentação não produzir efeito observável, é o caso de tipos trivialmente copiáveis.

LF
fonte
Concordo plenamente com o raciocínio lógico. Só não tenho certeza se isso está realmente no padrão? Você pode apoiar isso com alguma coisa?
Flamefire 13/11/19
@Flamefire Hmm ... Em geral, as funcionalidades padrão fornecem a garantia básica (a menos que haja algo errado com o que o usuário fornece), e std::variantnã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.
LF
Obrigado. Há um dentro especificação mais explícito: if an exception is thrown during the call toT’s constructor, valid()will be false;Assim que fez proibir este "otimização"
Flamefire
Sim. Especificação de emplaceem P0088 emException safety
Flamefire 13/11/19
@Flamefire Parece ser uma discrepância entre a proposta original e a versão votada. A versão final mudou para o texto "may".
LF