У мене є компонент, який я використовую при реалізації низькорівневих загальних типів, які зберігають об'єкт довільного типу (може бути або не бути типом класу), який може бути порожнім, щоб скористатися перевагами порожньої базової оптимізації :
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...>;
Останнім часом я возився із структурами даних, що не містять блокування, і мені потрібні вузли, які необов'язково містять дані в реальному часі. Після розподілу вузли живуть протягом усього строку структури даних, але вміщений реперний пункт живий лише тоді, коли вузол активний, а не поки вузол знаходиться у вільному списку. Я реалізував вузли, використовуючи необроблене зберігання та розміщення 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_;
};
це все чудово, але марно витрачає фрагмент пам'яті розміром із вказівник на кожен вузол, коли T
він порожній: один байт для raw_storage<T>::space_
і sizeof(std::atomic<list_node*>) - 1
байти відступів для вирівнювання. Було б непогано скористатися перевагами EBO та виділити невикористане однобайтове представлення raw_container<T>
зверху 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.");
Це рішення також може спричинити невизначену поведінку під час будівництва. У якийсь момент програма повинна сконструювати об'єкт зараженого в необробленому сховищі з розміщенням 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, щоб уникнути марної втрати місця в пам’яті для представлення вміщеного об’єкта?