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?
? With maybe more tests on
T` para asegurarse de que esté construido de manera vacía ... o de alguna manera para asegurarse de que pueda construirT
sin construirT
, asumiendo queT::T()
tiene efectos secundarios. ¿Tal vez una clase de rasgos para construidos / destruidos sin vacíoT
que diga cómo construir un vacíoT
?space_
matriz y usarlo de manera segura mientras no está construido en la lista libre? Entoncesspace_
no contendrá T sino algo de envoltura alrededor de T y el puntero atómico.Respuestas:
Creo que usted mismo dio la respuesta en sus diversas observaciones:
Estos requisitos son contradictorios. Por tanto, la respuesta es No , eso no es posible.
Sin embargo, podría cambiar un poco más sus requisitos al requerir la sobrecarga de cero bytes solo para tipos vacíos y triviales.
Podrías definir un nuevo rasgo de clase, p. Ej.
template <typename T> struct constructor_and_destructor_are_empty : std::false_type { };
Entonces te especializas
template <typename T, typename = void> class raw_container; template <typename T> class raw_container< T, std::enable_if_t< std::is_empty<T>::value and std::is_trivial<T>::value>> { public: T& data() noexcept { return reinterpret_cast<T&>(*this); } void construct() { // do nothing } void destruct() { // do nothing } }; template <typename T> struct list_node : public raw_container<T> { std::atomic<list_node*> next_; };
Entonces úsalo así:
using node = list_node<empty<char>>; static_assert(sizeof(node) == sizeof(std::atomic<node*>), "Good");
Por supuesto, todavía tienes
struct bar : raw_container<empty<char>> { empty<char> e; }; static_assert(sizeof(bar) == 1, "Yes, two objects sharing an address");
Pero eso es normal para EBO:
struct ebo1 : empty<char>, empty<usigned char> {}; static_assert(sizeof(ebo1) == 1, "Two object in one place"); struct ebo2 : empty<char> { char c; }; static_assert(sizeof(ebo2) == 1, "Two object in one place");
Pero siempre y cuando se utilice siempre
construct
ydestruct
ni la colocación de nuevo en&data()
, que es de oro.fuente
std::is_trivial
:-)