使用原始存储时如何模​​拟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>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.");

该解决方案还可能在构造时出现不确定的行为。在某个时候,程序必须在原始存储中通过放置来构造容器对象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如果容器类型是从包含的类型派生的,则构造/销毁容器对象必然会构造/销毁所包含的子对象。对于构造,这意味着您要么失去了预先分配容器对象的能力,要么必须延迟构造它们,直到准备构造容器为止。没什么大不了的,它只是增加了另一件事要跟踪-已分配但尚未构造的容器对象。用一个死掉的容器子对象销毁一个容器对象是一个更困难的问题,但是-如何避免基类的析构函数?
Casey 2014年

啊,对不起。忘记了用这种方法不可能进行延迟的构造/破坏,并且忘记了隐式的析构函数调用。
哥伦布2014年

`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它说如何空虚地构造一个T
Yakk-Adam Nevraumont 2015年

另一个想法:ebo存储类是否接受了不允许您将其视为空类型的列表,因为ebo存储类的地址会与之重叠吗?
Yakk-亚当·内夫罗蒙特2015年

1
在启动时,您将自动从空闲列表中提取项目,构造它,并将其自动放入跟踪列表中。拆卸时,您将自动从跟踪列表中删除,调用析构函数,然后自动插入空闲列表中。因此,在构造函数和析构函数调用时,原子指针未使用,可以自由修改,对吗?如果是这样,问题将是:您可以将原子指针放到space_数组中并在自由列表上未构造原子指针时安全地使用它吗?然后space_将不包含T,而是围绕T和原子指针的一些包装。
Speed8ump

Answers:


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:-)的力量
Rumburak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.