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

最近我一直在使用无锁数据结构,我需要可选地包含活动数据的节点。一旦分配,节点将在数据结构的生命周期内活动,但是所包含的数据只有在节点处于活动状态时才是活动的,而不是在节点位于空闲列表中时才是活动的。我使用原始存储和放置 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 并在 list_node::next_之上分配未使用的 raw_container<T>单字节表示,那就太好了。

我创建 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 来避免为表示所包含的对象而浪费内存空间?

1964 次浏览

我想你在各种各样的观察中已经给出了答案:

  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");

但是只要你总是使用 constructdestruct,并且在 &data()上没有新的位置,你就是黄金。