У меня есть компонент, который я использую при реализации низкоуровневых общих типов, которые хранят объект произвольного типа (может или не может быть типом класса), который может быть пуст, чтобы воспользоваться пустая оптимизация базы:
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...>;
В последнее время я возился с незакрепленными структурами данных, и мне нужны узлы, которые необязательно содержат живую датум. После выделения узлы живут на протяжении всей жизни структуры данных, но содержащаяся в ней информация остается в живых только тогда, когда активна node, а не тогда, когда node находится в свободном списке. Я реализовал узлы с использованием исходного хранилища и размещения 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_;
};
который все отлично и денди, но тратит блок размером с указателем на node, когда T
пуст: один байт для raw_storage<T>::space_
и sizeof(std::atomic<list_node*>) - 1
байты заполнения для выравнивания. Было бы неплохо воспользоваться EBO и выделить неиспользуемое однобайтовое представление raw_container<T>
atop list_node::next_
.
Моя лучшая попытка создания raw_ebo_storage
выполняет "ручной" EBO:
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)
));
}
};
который имеет желаемые эффекты:
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!");
но также и некоторые нежелательные эффекты, я предполагаю из-за нарушения строгой псевдонимы (3.10/10), хотя значение "доступ к сохраненному значению объекта" является дискуссионным для пустого типа:
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.");
Это решение также потенциально для поведения undefined при построении. В какой-то момент программа должна построить объект-хранилище в необработанном хранилище с размещением 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, "");
Вспомним, что, несмотря на то, что он пуст, полный объект обязательно имеет ненулевой размер. Другими словами, пустой полный объект имеет представление значения, состоящее из одного или нескольких байтов заполнения. new
строит полные объекты, поэтому соответствующая реализация может устанавливать эти байты заполнения на произвольные значения при построении, а не оставлять память нетронутой, как это было бы при построении пустого базового подобъекта. Это, конечно, было бы катастрофическим, если бы эти прописные байты накладывали другие живые объекты.
Таким образом, возникает вопрос: возможно ли создать стандартный класс контейнеров, который использует исходную инициализацию с сохранением/задержкой для содержащегося объекта и использует EBO, чтобы избежать потери пространства памяти для представления содержащегося объекта?