Подтвердить что ты не робот

Как эмулировать EBO при использовании необработанного хранилища?

У меня есть компонент, который я использую при реализации низкоуровневых общих типов, которые хранят объект произвольного типа (может или не может быть типом класса), который может быть пуст, чтобы воспользоваться пустая оптимизация базы:

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, чтобы избежать потери пространства памяти для представления содержащегося объекта?

4b9b3361

Ответ 1

Я думаю, что вы ответили сами в своих различных наблюдениях:

  • Вы хотите, чтобы исходная память и место размещения были новыми. Для этого требуется наличие хотя бы одного байта, даже если вы хотите создать пустой объект через новое место размещения.
  • Для хранения любых пустых объектов вам нужны служебные данные с нулевым байтом.

Эти требования противоречат друг другу. Поэтому ответ Нет, что невозможно.

Однако вы можете немного изменить свои требования, требуя накладных расходов нулевого байта только для пустых, тривиальных типов.

Можно определить новый признак класса, например

template <typename T>
struct constructor_and_destructor_are_empty : std::false_type
{
};

Затем вы специализируетесь на

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_;
};

Затем используйте его следующим образом:

using node = list_node<empty<char>>;
static_assert(sizeof(node) == sizeof(std::atomic<node*>), "Good");

Конечно, у вас все еще есть

struct bar : raw_container<empty<char>> { empty<char> e; };
static_assert(sizeof(bar) == 1, "Yes, two objects sharing an address");

Но это нормально для 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");

Но пока вы всегда используете construct и destruct и не размещаете новое на &data(), вы являетесь золотым.