Development Tip

원시 스토리지를 사용할 때 EBO를 에뮬레이트하는 방법은 무엇입니까?

yourdevel 2020. 10. 10. 12:08
반응형

원시 스토리지를 사용할 때 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비어 있을 때 노드 당 포인터 크기의 메모리 청크를 낭비합니다 .은 1 바이트 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, "");

Recall that despite being empty, a complete object necessarily has non-zero size. In other words, an empty complete object has a value representation that consists of one or more padding bytes. new constructs complete objects, so a conforming implementation could set those padding bytes to arbitrary values at construction instead of leaving memory untouched as would be the case for constructing an empty base subobject. This would of course be catastrophic if those padding bytes overlay other live objects.

So the question is, is it possible to create a standard-compliant container class that uses raw storage/delayed initialization for the contained object and takes advantage of EBO to avoid wasting memory space for the representation of the contained object?


I think you gave the answer yourself in your various observations:

  1. You want raw memory and placement new. This requires to have at least one byte available, even if you want to construct an empty object via placement new.
  2. You want zero bytes overhead for storing any empty objects.

These requirements are self-contradicting. The answer therefore is No, that is not possible.

You could change your requirements a bit more, though, by requiring the zero byte overhead only for empty, trivial types.

You could define a new class trait, e.g.

template <typename T>
struct constructor_and_destructor_are_empty : std::false_type
{
};

Then you specialize

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_;
};

Then use it like this:

using node = list_node<empty<char>>;
static_assert(sizeof(node) == sizeof(std::atomic<node*>), "Good");

Of course, you still have

struct bar : raw_container<empty<char>> { empty<char> e; };
static_assert(sizeof(bar) == 1, "Yes, two objects sharing an address");

But that is normal for 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");

But as long as you always use construct and destruct and no placement new on &data(), you're golden.

참고URL : https://stackoverflow.com/questions/26703289/how-to-emulate-ebo-when-using-raw-storage

반응형