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

비어 있음에도 불구하고 완전한 객체의 크기는 반드시 0이 아닙니다. 즉, 빈 완전한 객체에는 하나 이상의 패딩 바이트로 구성된 값 표현이 있습니다. new완전한 객체를 생성하므로 준수 구현은 빈 기본 하위 객체를 생성하는 경우처럼 메모리를 그대로 두는 대신 생성시 임의의 값으로 패딩 바이트를 설정할 수 있습니다. 물론 패딩 바이트가 다른 라이브 객체에 겹치면 치명적일 것입니다.

질문은 그래서, 그것은 포함 된 개체에 대한 사용 원료 저장 / 지연 초기화하는 표준 호환 컨테이너 클래스를 만들 수 있습니다 포함 된 객체의 표현을 위해 피하기 낭비 메모리 공간에 EBO을 활용?


@Columbo 컨테이너 유형이 포함 된 유형에서 파생 된 경우 컨테이너 개체를 구성 / 파괴하면 포함 된 하위 개체가 반드시 생성 / 파괴됩니다. 구성의 경우 컨테이너 개체를 미리 할당하는 기능을 잃거나 컨테이너를 구성 할 준비가 될 때까지 구성을 지연해야합니다. 큰 문제는 아니지만, 할당되었지만 아직 생성되지 않은 컨테이너 객체를 추적 할 또 다른 기능을 추가 할뿐입니다. 죽은 컨테이너 하위 개체가있는 컨테이너 개체를 파괴 하는 것은 더 어려운 문제입니다. 기본 클래스 소멸자를 어떻게 피합니까?
Casey 2014

아, 실례합니다. 지연된 건설 / 파괴는 이런 식으로 불가능하며 암시 적 소멸자 호출을 잊어 버렸습니다.
Columbo 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하지 않고 구성 할 수 있는지 확인하는 방법 입니다. 아마도 어떻게 ? TT::T()TT
Yakk-Adam Nevraumont 2015

또 다른 생각 : ebo 스토리지 클래스가 비어있는 것으로 처리 할 수없는 유형의 목록을 가져 오도록하세요. 비어있는 경우 ebo 스토리지 클래스의 주소가 겹칠 것이기 때문입니다.
Yakk-Adam Nevraumont 2015

1
불러 오기를하면 무료 목록에서 항목을 원자 적으로 가져 와서 구성한 다음 추적 목록에 원자 적으로 넣습니다. 분해시 추적 목록에서 원자 적으로 제거하고 소멸자를 호출 한 다음 자유 목록에 원자 적으로 삽입합니다. 그래서 생성자와 소멸자 호출에서 원자 포인터가 사용되지 않고 자유롭게 수정할 수 있습니까? 그렇다면 질문은 다음과 같습니다. 원자 포인터를 space_배열에 넣고 자유 목록에서 구성되지 않은 상태에서 안전하게 사용할 수 있습니까? 그런 다음 space_T를 포함하지 않지만 T와 원자 포인터를 둘러싼 래퍼를 포함합니다.
Speed8ump 2015

답변:


2

나는 당신이 다양한 관찰에서 스스로 대답했다고 생각합니다.

  1. 원시 메모리와 새로운 배치를 원합니다. 이것은 가질 필요 하나 이상의 바이트 새로운 배치를 통해 빈 객체를 생성 할 경우에도 사용할 수 있습니다.
  2. 당신이 원하는 제로 바이트 빈 객체를 저장하기위한 오버 헤드.

이러한 요구 사항은 모순됩니다. 대답은 그러므로 아니 불가능하다.

하지만 비어 있고 사소한 유형에 대해서만 0 바이트 오버 헤드를 요구하여 요구 사항을 조금 더 변경할 수 있습니다.

새로운 클래스 특성을 정의 할 수 있습니다.

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.