¿GCC9 evita el estado sin valor de std :: variant permitido?

14

Recientemente seguí una discusión de Reddit que condujo a una buena comparación de la std::visitoptimizació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_Icon los argumentos. std::forward<Args>(args)....Si se produce una excepción, *thispuede 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_copyableexcluye 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 emplaceno 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"?

Fuego de fuego
fuente

Respuestas:

7

Creo que la parte importante de la norma es esta:

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

23.7.3.4 Modificadores

(...)

template variant_alternative_t> & emplace (Args && ... args);

(...) Si se produce una excepción durante la inicialización del valor contenido, la variante podría no contener un valor

Dice "podría" no "debe". Esperaría que esto sea intencional para permitir implementaciones como la utilizada por gcc.

Como mencionó usted mismo, esto solo es posible si los destructores de todas las alternativas son triviales y, por lo tanto, no se pueden observar porque se requiere destruir el valor anterior.

Siguiente pregunta:

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

¿T tmp {std :: forward (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"?

Sí, porque para los tipos que se pueden copiar trivialmente no hay forma de detectar la diferencia, por lo que la implementación se comporta como si el valor se inicializara como se describe. Esto no funcionaría si el tipo no se pudiera copiar trivialmente.

PaulR
fuente
Interesante. Actualicé la pregunta con una solicitud de seguimiento / aclaración. La raíz es: ¿Está permitida la copia / mover? Estoy muy confundido por la might/mayredacción ya que el estándar no establece cuál es la alternativa.
Flamefire el
Aceptando esto para la cotización estándar y there is no way to detect the difference.
Flamefire el
5

Entonces, ¿permite el estándar que eso emplaceno cambie el valor actual?

Si. emplacedeberá 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).

variantse 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 tipo emplaceno 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.

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

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.

LF
fuente
Estoy totalmente de acuerdo con el razonamiento lógico. ¿No estoy seguro de si esto está realmente en el estándar? ¿Puedes respaldar esto con algo?
Flamefire el
@Flamefire Hmm ... En general, las funcionalidades estándar proporcionan la garantía básica (a menos que haya algo mal con lo que proporciona el usuario), y std::variantno 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.
LF
Gracias. Hay una especificación más explícita en el interior: if an exception is thrown during the call toT’s constructor, valid()will be false;Entonces, eso prohibió esta "optimización"
Flamefire
Si. Especificación de emplaceen P0088 bajoException safety
Flamefire
@Flamefire parece ser una discrepancia entre la propuesta original y la versión votada. La versión final cambió a la redacción "mayo".
LF