Як імітувати EBO при використанні необробленого сховища?


79

У мене є компонент, який я використовую при реалізації низькорівневих загальних типів, які зберігають об'єкт довільного типу (може бути або не бути типом класу), який може бути порожнім, щоб скористатися перевагами порожньої базової оптимізації :

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_storageEBO "вручну":

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, щоб уникнути марної втрати місця в пам’яті для представлення вміщеного об’єкта?


@Columbo Якщо тип контейнера є похідним від вміщеного типу, побудова / знищення об'єкта контейнера обов'язково створює / знищує вміщений суб'єкт. Для будівництва це означає, що ви або втрачаєте можливість попередньо розподіляти контейнерні об'єкти, або повинні відкласти їх будівництво, поки не будете готові побудувати контактана. Не біда, це просто додає ще одну річ для відстеження - виділені, але ще не побудовані об’єкти контейнера. Знищення об'єкта-контейнера мертвим суб'єктом контагента є складнішою проблемою, однак - як уникнути деструктора базового класу?
Кейсі

Ах, вибачте мене там. Забув, що затримка будівництва / руйнування неможлива таким чином, і неявний виклик деструктора.
Коламбо

`template <typename T> struct alignas (T) raw_ebo_storage_base <T, std :: enable_if_t <std :: is_empty <T> :: value>>: T {}; ? With maybe more tests on T`, щоб переконатися, що вона побудована порожньо ... або якийсь спосіб переконатися, що ви можете будувати Tбез побудови T, припускаючи, що це T::T()має побічні ефекти. Можливо клас ознак для невакуально побудованих / знищених, Tякий говорить про те, як вакуумно побудувати a T?
Якк - Адам Неврамонт,

Ще одна думка: нехай клас зберігання ebo бере список типів, до яких ви не маєте права ставитись порожніми, оскільки адреса класу зберігання ebo перекриватиметься з ним, якщо він це зробить?
Якк - Адам Неврамонт

1
Під час відновлення ви атомно витягнете елемент із вільного списку, сконструюєте його та атомарно внесете до списку відстеження. Після розірвання ви будете атомно видаляти зі списку відстеження, викликати деструктор, а потім атомно вставляти у вільний список. Отже, при викликах конструктора та деструктора атомний вказівник не використовується, і його можна вільно модифікувати, правильно? Якщо так, то запитання буде: чи можете ви помістити атомарний покажчик у space_масив і безпечно використовувати його, поки він не побудований у вільному списку? Тоді space_не буде містити T, але якась обгортка навколо T та атомний вказівник.
Speed8ump

Відповіді:


2

Думаю, ви самі дали відповідь у своїх різних спостереженнях:

  1. Вам потрібна нова пам’ять та розміщення. Для цього потрібно мати принаймні один байт , навіть якщо ви хочете побудувати порожній об’єкт за допомогою розміщення new.
  2. Вам потрібні нульові байти накладних витрат для зберігання будь-яких порожніх об'єктів.

Ці вимоги самі собою суперечать. Отже, відповідь - Ні , це неможливо.

Ви можете трохи більше змінити свої вимоги, вимагаючи накладних витрат нульового байту лише для порожніх тривіальних типів.

Ви можете визначити нову ознаку класу, наприклад

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(), ви золотий.


Дякую @Deduplicator за те, що я усвідомив силу std::is_trivial:-)
Румбурак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.