我有一个实现低级泛型类型时使用的组件,该泛型类型存储任意类型的对象(可能是也可能不是类类型),该对象可能为空以利用空基础优化:
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来避免浪费内存空间来表示包含的对象?