Recientemente seguí una discusión de Reddit que condujo a una buena comparación de la std::visit
optimización entre los compiladores. Noté lo siguiente: https://godbolt.org/z/D2Q5ED
Tanto GCC9 como Clang9 (supongo que comparten el mismo stdlib) no generan código para verificar y lanzar una excepción sin valor cuando todos los tipos cumplen algunas condiciones. Esto conduce a un mejor codegen, por lo tanto, planteé un problema con el MSVC STL y me presentaron 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&) {}
}
La afirmación era que esto hace que cualquier variante no tenga valor, y al leer el documento debería:
Primero, destruye el valor actualmente contenido (si lo hay). Luego, inicializa directamente el valor contenido como si construyera un valor de tipo
T_I
con los argumentos.std::forward<Args>(args)....
Si se produce una excepción,*this
puede convertirse en valueless_by_exception.
Lo que no entiendo: ¿por qué se dice "puede"? ¿Es legal permanecer en el estado anterior si se produce toda la operación? Porque esto es lo que hace GCC:
// 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>>
{ };
Y luego (condicionalmente) hace algo como:
T tmp = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);
Por lo tanto, básicamente crea un temporal, y si eso tiene éxito, lo copia / mueve al lugar real.
OMI, esto es una violación de "Primero, destruye el valor actualmente contenido" como se indica en el documento. Mientras leo el estándar, después de un v.emplace(...)
valor actual en la variante siempre se destruye y el nuevo tipo es el tipo establecido o no tiene valor.
Sí entiendo que la condición is_trivially_copyable
excluye todos los tipos que tienen un destructor observable. Entonces, esto también puede ser como: "la variante as-if se reinicializa con el valor anterior" más o menos. Pero el estado de la variante es un efecto observable. Entonces, ¿permite el estándar que eso emplace
no cambie el valor actual?
Edite en respuesta a una cotización estándar:
Luego inicializa el valor contenido como si inicializara sin listar un valor de tipo TI con los argumentos
std::forward<Args>(args)...
.
¿ T tmp {std::forward<Args>(args)...}; this->value = std::move(tmp);
Realmente cuenta como una implementación válida de lo anterior? ¿Es esto lo que se entiende por "como si"?
fuente
might/may
redacción ya que el estándar no establece cuál es la alternativa.there is no way to detect the difference
.Si.
emplace
deberá proporcionar la garantía básica de que no hay fugas (es decir, respetar la vida útil del objeto cuando la construcción y la destrucción producen efectos secundarios observables), pero cuando sea posible, se le permite proporcionar una garantía sólida (es decir, el estado original se mantiene cuando falla una operación).variant
se requiere que se comporte de manera similar a una unión: las alternativas se asignan en una región de almacenamiento adecuadamente asignado. No está permitido asignar memoria dinámica. Por lo tanto, un cambio de tipoemplace
no tiene forma de mantener el objeto original sin llamar a un constructor de movimiento adicional: tiene que destruirlo y construir el nuevo objeto en su lugar. Si esta construcción falla, entonces la variante debe pasar al estado excepcional sin valor. Esto evita cosas raras como destruir un objeto inexistente.Sin embargo, para los tipos pequeños que se pueden copiar trivialmente, es posible proporcionar una garantía sólida sin demasiados gastos generales (incluso un aumento de rendimiento para evitar un control, en este caso). Por lo tanto, la implementación lo hace. Esto es conforme a la norma: la implementación aún proporciona la garantía básica requerida por la norma, solo que de una manera más fácil de usar.
Sí, si la asignación de movimiento no produce ningún efecto observable, como es el caso de los tipos que se pueden copiar trivialmente.
fuente
std::variant
no tiene ninguna razón para romper eso. Estoy de acuerdo en que esto se puede hacer más explícito en la redacción del estándar, pero así es básicamente cómo funcionan otros que forman parte de la biblioteca estándar. Y para su información, P0088 fue la propuesta inicial.if an exception is thrown during the call toT’s constructor, valid()will be false;
Entonces, eso prohibió esta "optimización"emplace
en P0088 bajoException safety