informacion - for c++ 17
¿Por qué no hay un std:: construct_at en C++ 17? (6)
Creo que debería haber una función constructiva estándar.
De hecho, libc ++ tiene uno como detalle de implementación en el archivo
stl_construct.h
.
namespace std{
...
template<typename _T1, typename... _Args>
inline void
_Construct(_T1* __p, _Args&&... __args)
{ ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
...
}
Creo que es algo útil tener porque permite hacer de la "nueva ubicación" un amigo.
Este es un gran punto de personalización para un tipo de solo movimiento que necesita una
uninitialized_copy
en el montón predeterminado (desde un elemento
std::initializer_list
, por ejemplo).
Tengo mi propia biblioteca de contenedores que reimplementa un
detail::uninitialized_copy
(de un rango) para usar un
detail::construct
personalizado
detail::construct
:
namespace detail{
template<typename T, typename... As>
inline void construct(T* p, As&&... as){
::new(static_cast<void*>(p)) T(std::forward<As>(as)...);
}
}
Que se declara amigo de una clase de solo movimiento para permitir la copia solo en el contexto de la ubicación nueva.
template<class T>
class my_move_only_class{
my_move_only_class(my_move_only_class const&) = default;
friend template<class TT, class...As> friend void detail::construct(TT*, As&&...);
public:
my_move_only_class(my_move_only_class&&) = default;
...
};
C ++ 17 agrega
std::destroy_at
, pero no hay ninguna contrapartida de
std::construct_at
.
¿Porqué es eso?
¿No podría ser implementado de la siguiente manera?
template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
return new (addr) T(std::forward<Args>(args)...);
}
Lo que permitiría evitar esa nueva sintaxis de colocación no completamente natural :
auto ptr = construct_at<int>(buf, 1); // instead of ''auto ptr = new (buf) int(1);''
std::cout << *ptr;
std::destroy_at(ptr);
Existe tal cosa, pero no se nombra como se podría esperar :
-
uninitialized_copy copia un rango de objetos a un área de memoria no inicializada
-
uninitialized_copy_n (C ++ 11) copia varios objetos a un área de memoria no inicializada (plantilla de función)
-
uninitialized_fill copia un objeto en un área de memoria no inicializada, definida por un rango (plantilla de función)
- uninitialized_fill_n copia un objeto en un área de memoria no inicializada, definida por un inicio y un conteo (plantilla de función)
- uninitialized_move (C ++ 17) mueve un rango de objetos a un área sin inicializar de la memoria (plantilla de función)
- uninitialized_move_n (C ++ 17) mueve una serie de objetos a un área de memoria no inicializada (plantilla de función)
- uninitialized_default_construct (C ++ 17) construye objetos por inicialización predeterminada en un área de memoria no inicializada, definida por un rango (plantilla de función)
- uninitialized_default_construct_n (C ++ 17) construye objetos por inicialización predeterminada en un área de memoria no inicializada, definida por un inicio y un recuento (plantilla de función)
- uninitialized_value_construct (C ++ 17) construye objetos por inicialización de valores en un área de memoria no inicializada, definida por un rango (plantilla de función)
- uninitialized_value_construct_n (C ++ 17) construye objetos por inicialización de valores en un área de memoria no inicializada, definida por un inicio y un conteo
Hay
std::allocator_traits::construct
.
Solía haber uno más en
std::allocator
, pero se eliminó, razón en
el documento del comité de normas D0174R0
.
construct
no parece proporcionar ningún azúcar sintáctico.
Además es menos eficiente que una colocación nueva.
Los enlaces a los argumentos de referencia causan materialización temporal y construcción adicional de movimiento / copia:
struct heavy{
unsigned char[4096];
heavy(const heavy&);
};
heavy make_heavy(); // Return a pr-value
auto loc = ::operator new(sizeof(heavy));
// Equivalently: unsigned char loc[sizeof(heavy)];
auto p = construct<heavy>(loc,make_heavy()); // The pr-value returned by
// make_heavy is bound to the second argument,
// and then this arugment is copied in the body of construct.
auto p2 = new(loc) auto(make_heavy()); // Heavy is directly constructed at loc
//... and this is simpler to write!
Desafortunadamente, no hay ninguna forma de evitar estas construcciones adicionales de copia / movimiento al llamar a una función. El reenvío es casi perfecto.
Por otro lado,
construct_at
en la biblioteca podría completar el vocabulario estándar de la biblioteca.
std::construct_at
se ha agregado a C ++ 20.
El papel que lo hizo es
contenedores más constexpr
.
Presumiblemente, no se vio que esto tuviera suficientes ventajas sobre la ubicación nueva en C ++ 17, pero C ++ 20 cambia las cosas.
El propósito de la propuesta que agregó esta función es admitir las asignaciones de memoria constexpr, incluido
std::vector
.
Esto requiere la capacidad de construir objetos en el almacenamiento asignado.
Sin embargo, simplemente coloque nuevas ofertas en términos de
void *
, no
T *
.
constexpr
evaluación
constexpr
no tiene capacidad para acceder al almacenamiento sin
constexpr
, y el comité quiere mantenerlo así.
La función de biblioteca
std::construct_at
agrega una interfaz escrita
constexpr T * construct_at(T *, Args && ...)
.
Esto también tiene la ventaja de no requerir que el usuario especifique el tipo que se está construyendo;
Se deduce del tipo de puntero.
La sintaxis para llamar correctamente a la ubicación nueva es algo horrible y contraintuitiva.
Compare
std::construct_at(ptr, args...)
con
::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...)
.
std::destroy_at
proporciona dos mejoras objetivas sobre una llamada de destructor directo:
-
Reduce la redundancia:
T *ptr = new T; //Insert 1000 lines of code here. ptr->~T(); //What type was that again?
Claro, todos preferiríamos simplemente envolverlo en un
unique_ptr
y terminar con él, pero si eso no puede suceder por alguna razón, ponerT
allí es un elemento de redundancia. Si cambiamos el tipo aU
, ahora tenemos que cambiar la llamada del destructor o las cosas se rompen. El uso destd::destroy_at(ptr)
elimina la necesidad de cambiar lo mismo en dos lugares.Lo seco es bueno.
-
Esto lo hace fácil:
auto ptr = allocates_an_object(...); //Insert code here ptr->~???; //What type is that again?
Si deducimos el tipo de puntero, borrarlo se vuelve un poco difícil. No puedes hacer
ptr->~decltype(ptr)()
; ya que el analizador de C ++ no funciona de esa manera. No solo eso,decltype
deduce el tipo como un puntero , por lo que debería eliminar un direccionamiento indirecto del puntero del tipo deducido. Guiándote aauto ptr = allocates_an_object(...); //Insert code here using delete_type = std::remove_pointer_t<decltype(ptr)>; ptr->~delete_type();
¿Y quién quiere escribir eso ?
Por el contrario, su hipotético
std::construct_at
no proporciona mejoras
objetivas
sobre la ubicación
new
.
Tienes que indicar el tipo que estás creando en ambos casos.
Los parámetros al constructor deben ser provistos en ambos casos.
El puntero a la memoria debe proporcionarse en ambos casos.
Así que no hay necesidad de ser resuelto por su hipotético
std::construct_at
.
Y es objetivamente menos capaz que la colocación nueva. Puedes hacerlo:
auto ptr1 = new(mem1) T;
auto ptr2 = new(mem2) T{};
Estos son diferentes En el primer caso, el objeto está inicializado por defecto, lo que puede dejarlo sin inicializar. En el segundo caso, el objeto está inicializado por valores.
Su hipotético
std::construct_at
no puede
permitirle elegir cuál desea.
Puede tener un código que realice la inicialización predeterminada si no proporciona parámetros, pero no podrá proporcionar una versión para la inicialización de valores.
Y se podría inicializar el valor sin parámetros, pero luego no se pudo inicializar el objeto por defecto.