Tengo un componente que uso al implementar tipos genéricos de bajo nivel que almacenan un objeto de tipo arbitrario (puede o no ser un tipo de clase) que puede estar vacío para aprovechar la optimización de base vacía :
template <typename T, unsigned Tag = 0, typename = void>
class ebo_storage {
T item;
public:
constexpr ebo_storage() = default;
template <
typename U,
typename = std::enable_if_t<
!std::is_same<ebo_storage, std::decay_t<U>>::value
>
> constexpr ebo_storage(U&& u)
noexcept(std::is_nothrow_constructible<T,U>::value) :
item(std::forward<U>(u)) {}
T& get() & noexcept { return item; }
constexpr const T& get() const& noexcept { return item; }
T&& get() && noexcept { return std::move(item); }
};
template <typename T, unsigned Tag>
class ebo_storage<
T, Tag, std::enable_if_t<std::is_class<T>::value>
> : private T {
public:
using T::T;
constexpr ebo_storage() = default;
constexpr ebo_storage(const T& t) : T(t) {}
constexpr ebo_storage(T&& t) : T(std::move(t)) {}
T& get() & noexcept { return *this; }
constexpr const T& get() const& noexcept { return *this; }
T&& get() && noexcept { return std::move(*this); }
};
template <typename T, typename U>
class compressed_pair : ebo_storage<T, 0>,
ebo_storage<U, 1> {
using first_t = ebo_storage<T, 0>;
using second_t = ebo_storage<U, 1>;
public:
T& first() { return first_t::get(); }
U& second() { return second_t::get(); }
// ...
};
template <typename, typename...> class tuple_;
template <std::size_t...Is, typename...Ts>
class tuple_<std::index_sequence<Is...>, Ts...> :
ebo_storage<Ts, Is>... {
// ...
};
template <typename...Ts>
using tuple = tuple_<std::index_sequence_for<Ts...>, Ts...>;
Últimamente he estado jugando con estructuras de datos sin bloqueo y necesito nodos que, opcionalmente, contengan un dato en vivo. Una vez asignados, los nodos viven durante la vida útil de la estructura de datos, pero el dato contenido solo está vivo mientras el nodo está activo y no mientras el nodo se encuentra en una lista libre. Implementé los nodos usando almacenamiento y ubicación sin procesar new
:
template <typename T>
class raw_container {
alignas(T) unsigned char space_[sizeof(T)];
public:
T& data() noexcept {
return reinterpret_cast<T&>(space_);
}
template <typename...Args>
void construct(Args&&...args) {
::new(space_) T(std::forward<Args>(args)...);
}
void destruct() {
data().~T();
}
};
template <typename T>
struct list_node : public raw_container<T> {
std::atomic<list_node*> next_;
};
que está muy bien, pero desperdicia una porción de memoria del tamaño de un puntero por nodo cuando T
está vacío: un byte para raw_storage<T>::space_
y sizeof(std::atomic<list_node*>) - 1
bytes de relleno para la alineación. Sería bueno aprovechar EBO y asignar la representación de raw_container<T>
un solo byte no utilizada de encima list_node::next_
.
Mi mejor intento de crear un raw_ebo_storage
EBO "manual" realiza:
template <typename T, typename = void>
struct alignas(T) raw_ebo_storage_base {
unsigned char space_[sizeof(T)];
};
template <typename T>
struct alignas(T) raw_ebo_storage_base<
T, std::enable_if_t<std::is_empty<T>::value>
> {};
template <typename T>
class raw_ebo_storage : private raw_ebo_storage_base<T> {
public:
static_assert(std::is_standard_layout<raw_ebo_storage_base<T>>::value, "");
static_assert(alignof(raw_ebo_storage_base<T>) % alignof(T) == 0, "");
T& data() noexcept {
return *static_cast<T*>(static_cast<void*>(
static_cast<raw_ebo_storage_base<T>*>(this)
));
}
};
que tiene los efectos deseados:
template <typename T>
struct alignas(T) empty {};
static_assert(std::is_empty<raw_ebo_storage<empty<char>>>::value, "Good!");
static_assert(std::is_empty<raw_ebo_storage<empty<double>>>::value, "Good!");
template <typename T>
struct foo : raw_ebo_storage<empty<T>> { T c; };
static_assert(sizeof(foo<char>) == 1, "Good!");
static_assert(sizeof(foo<double>) == sizeof(double), "Good!");
pero también algunos efectos indeseables, supongo que se deben a la violación del aliasing estricto (3.10 / 10) aunque el significado de "acceder al valor almacenado de un objeto" es discutible para un tipo vacío:
struct bar : raw_ebo_storage<empty<char>> { empty<char> e; };
static_assert(sizeof(bar) == 2, "NOT good: bar::e and bar::raw_ebo_storage::data() "
"are distinct objects of the same type with the "
"same address.");
Esta solución también tiene potencial para un comportamiento indefinido durante la construcción. En algún momento, el programa debe construir el objeto contenedor dentro del almacenamiento sin procesar con la ubicación new
:
struct A : raw_ebo_storage<empty<char>> { int i; };
static_assert(sizeof(A) == sizeof(int), "");
A a;
a.value = 42;
::new(&a.get()) empty<char>{};
static_assert(sizeof(empty<char>) > 0, "");
Recuerde que a pesar de estar vacío, un objeto completo necesariamente tiene un tamaño distinto de cero. En otras palabras, un objeto completo vacío tiene una representación de valor que consta de uno o más bytes de relleno. new
construye objetos completos, por lo que una implementación conforme podría establecer esos bytes de relleno en valores arbitrarios en la construcción en lugar de dejar la memoria intacta como sería el caso para construir un subobjeto base vacío. Por supuesto, esto sería catastrófico si esos bytes de relleno se superpusieran a otros objetos activos.
Entonces, la pregunta es, ¿es posible crear una clase de contenedor que cumpla con los estándares que use almacenamiento sin procesar / inicialización retrasada para el objeto contenido y aproveche EBO para evitar desperdiciar espacio de memoria para la representación del objeto contenido?