如何在使用原始存储时模拟EBO?
在实现存储任意types的对象(可以是或不可以是类types)的低级genericstypes时,我使用了一个组件,它可能是空的,以利用空基优化 :
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
字节sizeof(std::atomic<list_node*>) - 1
字节的填充alignment。 利用EBO并将未使用的raw_container<T>
单字节表示分配给list_node::next_
上list_node::next_
。
我最好的尝试创build一个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),尽pipe“访问对象的存储值”的含义对于空types是有争议的:
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.");
这种解决scheme也可能在施工时出现未定义的行为。 在某个时候,程序必须在原始存储中构buildnew
存储区对象,
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, "");
回想一下,尽pipe是空的,一个完整的对象必然具有非零大小。 换句话说,一个空的完整对象具有由一个或多个填充字节组成的值表示。 new
构造完整的对象,所以一致的实现可以在构造时将这些填充字节设置为任意值,而不是像构build空的基础子对象那样不留下内存。 如果这些填充字节覆盖其他活动对象,这当然是灾难性的。
所以问题是,是否有可能创build一个兼容标准的容器类,对所包含的对象使用原始存储/延迟初始化, 并利用EBO避免为包含对象的表示浪费内存空间?
我想你在各种观察中自己给出了答案:
- 你想要新的内存和位置。 这需要至less有一个字节可用,即使你想通过新的位置构build一个空的对象。
- 你需要零字节的开销来存储任何空的对象。
这些要求是自相矛盾的。 所以答案是否定的 ,这是不可能的。
你可以改变你的需求,但要求零字节开销只用于空的,平凡的types。
你可以定义一个新的类特质,例如
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()
,你是黄金。