¿Por qué std::aligned_storage está en desuso en C++ 23 y qué usar en su lugar?

5 minutos de lectura

Acabo de ver que C++23 planea desaprobar ambos std::aligned_storage y std::aligned_storage_t tanto como std::aligned_union y std::aligned_union_t.

La colocación de objetos nuevos en almacenamiento alineado no es particularmente constexpr amigable, según tengo entendido, pero esa no parece ser una buena razón para descartar el tipo por completo. Esto me lleva a suponer que hay algún otro problema fundamental con el uso std::aligned_storage y amigos de los que no estoy al tanto. ¿Qué sería eso?

¿Y hay una alternativa propuesta a estos tipos?

  • La propuesta pertinente parece ser P1413R3 – Desactivar std::aligned_storage y std::aligned_unionque de hecho fue aprobado el 7 de febrero de 2022

    – Brian

    11 abr a las 13:08


  • Hay una nota en el estándar. “Los usos de added_storage​::​type pueden ser reemplazados por una matriz std​::​byte[Len] declarado con alignas(Align).”

    – Transeúnte

    11 abr a las 13:12

  • Me pregunto por qué piensan eso “Usando aligned_* invoca un comportamiento indefinido”. He hecho una pregunta sobre esto: ¿Por qué el uso de std::aligned_storage supuestamente causa UB debido a que no “proporciona almacenamiento”?

    – Santo Gato Negro

    11 abr a las 22:06


avatar de usuario
Ted Lyngmo

Aquí hay tres extractos de P1413R3:

Fondo

aligned_* son perjudiciales para las bases de código y no deben utilizarse. A un nivel alto:

  • Usando aligned_* invoca un comportamiento indefinido (los tipos no pueden proporcionar almacenamiento).
  • Las garantías son incorrectas (el estándar solo requiere que el tipo sea al menos tan grande como se solicita, pero no establece un límite superior para el tamaño).
  • La API es incorrecta por una gran cantidad de razones (consulte “En la API”.)
  • Debido a que la API es incorrecta, casi todo el uso implica el mismo trabajo previo repetido (consulte “Uso existente”).

en la API

std::aligned_* sufren muchas malas decisiones de diseño de API. Algunos de estos son compartidos, y algunos son específicos de cada uno. En cuanto a lo que se comparte, hay tres problemas principales [only one is included here for brevity]:

  • Usando reinterpret_cast es necesario para acceder al valor

No hay .data() o incluso .data en std::aligned_* instancias. En cambio, la API requiere que tome la dirección del objeto, llame reinterpret_cast<T*>(...) con él, y luego indirectamente el puntero resultante que le da un T&. Esto no sólo significa que no se puede utilizar en constexprpero en tiempo de ejecución es mucho más fácil invocar accidentalmente un comportamiento indefinido. reinterpret_cast ser un requisito para el uso de una API es inaceptable.


Reemplazo sugerido

El reemplazo más fácil para aligned_* en realidad no es una característica de la biblioteca. En su lugar, los usuarios deben utilizar una matriz correctamente alineada de std::bytepotencialmente con una llamada a std::max(std::initializer_list<T>) . Estos se pueden encontrar en el
<cstddef> y <algorithm> encabezados, respectivamente (con ejemplos al final de esta sección). Desafortunadamente, este reemplazo no es ideal. Para acceder al valor de aligned_*los usuarios deben llamar reinterpret_cast en la dirección para leer los bytes como T instancias. El uso de una matriz de bytes como reemplazo no evita este problema. Dicho esto, es importante reconocer que continuar usando reinterpret_cast donde ya existe no es tan malo como introducirlo donde antes no estaba presente. …

El apartado anterior de la propuesta aceptada de jubilación aligned_* luego se sigue con una serie de ejemplos, como estas dos sugerencias de reemplazo:

// To replace std::aligned_storage
template <typename T>
class MyContainer {
private:
    //std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
    alignas(T) std::byte t_buff[sizeof(T)];
};
// To replace std::aligned_union
template <typename... Ts>
class MyContainer {
private:
    //std::aligned_union_t<0, Ts...> t_buff;
    alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
};

  • Agregar la sección “antecedentes” mejoró mucho la respuesta, gracias.

    – François Andrieux

    11 abr a las 13:25


  • @bitmask Sí, así es como yo también lo veo. Ambas cosas aligned_* y un alineado byte matriz requiere reinterpret_cast en algún momento. Agregué la sección de la propuesta sobre lo que termina con reinterpret_cast ser un requisito para el uso de una API es inaceptable” – así que supongo que tenían sentimientos bastante fuertes en contra de eso 🙂

    – Ted Lyngmo

    11 abr a las 14:08


  • Asimismo, el argumento de que reinterpret_cast es una parte necesaria de usar esta expresión/tipo es falso, porque la ubicación nueva devolverá un valor adecuado T*. Entonces, un caso de uso que nunca requiera reinterpretar ningún punto es concebible y plausible. De todos modos, se ha ido ahora y la gente tendrá que rodar por su cuenta.

    – máscara de bits

    11 abr a las 14:34

  • Lo que realmente necesitamos es una nueva forma de declarar un correctamente escrito matriz sin construir sus elementos todavía, por ejemplo: [[uninitialized_storage]] T t_arr[len]; donde el compilador asignaría la matriz de tamaño y alineación suficientes, simplemente no llamaría a ningún constructor. permitiendo que el desarrollador use placement-new para inicializar cada T elemento según sea necesario. Lo sé, ilusiones…

    – Rémy Lebeau

    11 abr a las 22:40


  • @TedLyngmo Si entiendo la nota al pie de esta respuesta de Barry, C ++ 23 debería poder hacer esto, pero no he estado siguiendo c ++ 23 tan de cerca. La alternativa que definitivamente funcionaría sería lanzar una estructura trivial vacía en la unión para ser miembro activo cuando T no es (por ejemplo union { struct empty_type {} empty; T entry; } t_arr[len]. Después std::construct_at cambia legalmente al miembro activo, y entry todavía se puede acceder en contextos constexpr sin reinterpret_cast siendo necesario

    – Compilador humano

    20 abr a las 18:51


¿Ha sido útil esta solución?