Làm thế nào để mô phỏng EBO khi sử dụng lưu trữ thô?


79

Tôi có một thành phần tôi sử dụng khi triển khai các kiểu chung cấp thấp lưu trữ một đối tượng thuộc kiểu tùy ý (có thể hoặc không phải là kiểu lớp) có thể trống để tận dụng tối ưu hóa cơ sở trống :

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...>;

Gần đây, tôi đã gặp rắc rối với các cấu trúc dữ liệu không có khóa và tôi cần các nút có tùy chọn chứa dữ liệu trực tiếp. Sau khi được cấp phát, các nút tồn tại trong suốt thời gian tồn tại của cấu trúc dữ liệu nhưng dữ liệu được chứa chỉ tồn tại trong khi nút đang hoạt động chứ không phải khi nút nằm trong danh sách miễn phí. Tôi đã triển khai các nút bằng cách sử dụng bộ nhớ thô và vị trí 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ất cả đều tốt và đẹp, nhưng lãng phí một phần bộ nhớ có kích thước bằng con trỏ cho mỗi nút khi Ttrống: một byte cho raw_storage<T>::space_sizeof(std::atomic<list_node*>) - 1byte đệm cho căn chỉnh. Sẽ rất tốt nếu tận dụng EBO và phân bổ biểu diễn byte đơn không sử dụng của raw_container<T>atop list_node::next_.

Nỗ lực tốt nhất của tôi trong việc tạo raw_ebo_storageEBO "thủ công" có hiệu suất:

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

mà có các hiệu ứng mong muốn:

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

nhưng cũng có một số tác dụng không mong muốn, tôi cho rằng do vi phạm bí danh nghiêm ngặt (3.10 / 10) mặc dù ý nghĩa của "truy cập giá trị được lưu trữ của một đối tượng" còn gây tranh cãi đối với kiểu trống:

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

Giải pháp này cũng tiềm ẩn các hành vi không xác định khi xây dựng. Tại một số thời điểm, chương trình phải xây dựng đối tượng container trong kho lưu trữ thô với vị trí 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, "");

Nhớ lại rằng mặc dù rỗng, một đối tượng hoàn chỉnh nhất thiết phải có kích thước khác không. Nói cách khác, một đối tượng hoàn chỉnh rỗng có một biểu diễn giá trị bao gồm một hoặc nhiều byte đệm. newxây dựng các đối tượng hoàn chỉnh, vì vậy việc triển khai tuân thủ có thể đặt các byte đệm đó thành các giá trị tùy ý khi xây dựng thay vì để bộ nhớ không bị ảnh hưởng như trường hợp xây dựng một subobject cơ sở trống. Điều này tất nhiên sẽ là thảm họa nếu những byte đệm đó chồng lên các vật thể sống khác.

Vì vậy, câu hỏi đặt ra là có thể tạo một lớp vùng chứa tuân thủ tiêu chuẩn sử dụng lưu trữ thô / khởi tạo chậm cho đối tượng được chứa tận dụng EBO để tránh lãng phí không gian bộ nhớ cho việc biểu diễn đối tượng được chứa không?


@Columbo Nếu kiểu vùng chứa có nguồn gốc từ kiểu chứa, việc xây dựng / phá hủy một đối tượng vùng chứa nhất thiết phải xây dựng / phá hủy đối tượng chứa được chứa. Đối với việc xây dựng, điều đó có nghĩa là bạn mất khả năng phân bổ trước các đối tượng vùng chứa hoặc phải trì hoãn việc xây dựng chúng cho đến khi bạn sẵn sàng xây dựng một vùng chứa. Không phải là một vấn đề lớn, nó chỉ thêm một thứ khác để theo dõi - các đối tượng vùng chứa được phân bổ nhưng chưa được xây dựng. Tuy nhiên, việc hủy một đối tượng vùng chứa bằng subobject vùng chứa đã chết là một vấn đề khó hơn - làm thế nào để bạn tránh được trình hủy lớp cơ sở?
Casey

À, xin lỗi. Quên rằng việc xây dựng / phá hủy bị trì hoãn là không thể theo cách này và lệnh gọi hàm hủy ngầm.
Columbo

`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` để đảm bảo nó được xây dựng trống ... hoặc một số cách để đảm bảo rằng bạn có thể xây dựng Tmà không cần xây dựng T, giả sử T::T()có tác dụng phụ. Có thể một lớp đặc điểm cho cấu trúc không trống / bị phá hủy Tcho biết cách cấu tạo trống Tkhông?
Yakk - Adam Nevraumont

Một suy nghĩ khác: hãy để lớp lưu trữ ebo có danh sách các kiểu bạn không được phép coi là trống, vì địa chỉ của lớp lưu trữ ebo sẽ trùng với nó nếu có?
Yakk - Adam Nevraumont

1
Khi mang lên, bạn sẽ tự động lấy một mục từ danh sách miễn phí, xây dựng nó và đưa nó vào danh sách theo dõi. Khi xé nhỏ, bạn sẽ loại bỏ nguyên tử khỏi danh sách theo dõi, gọi một trình hủy và sau đó chèn nguyên tử vào danh sách miễn phí. Vì vậy, tại các cuộc gọi hàm tạo và hàm hủy, con trỏ nguyên tử không được sử dụng và có thể được sửa đổi tự do, đúng không? Nếu vậy, câu hỏi sẽ là: bạn có thể đặt con trỏ nguyên tử vào space_mảng và sử dụng nó một cách an toàn trong khi nó không được cấu trúc trong danh sách miễn phí không? Sau đó space_sẽ không chứa T mà là một số bao bọc xung quanh T và con trỏ nguyên tử.
Speed8ump

Câu trả lời:


2

Tôi nghĩ rằng bạn đã tự đưa ra câu trả lời trong các quan sát khác nhau của mình:

  1. Bạn muốn bộ nhớ thô và vị trí mới. Điều này yêu cầu phải có sẵn ít nhất một byte , ngay cả khi bạn muốn tạo một đối tượng trống thông qua vị trí mới.
  2. Bạn muốn chi phí bằng không byte để lưu trữ bất kỳ đối tượng trống nào.

Những yêu cầu này là tự mâu thuẫn. Do đó, câu trả lời là Không , điều đó là không thể.

Tuy nhiên, bạn có thể thay đổi các yêu cầu của mình nhiều hơn một chút bằng cách chỉ yêu cầu chi phí byte 0 cho các loại trống, nhỏ.

Bạn có thể xác định một đặc điểm lớp mới, ví dụ:

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

Sau đó, bạn chuyên

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

Sau đó, sử dụng nó như thế này:

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

Tất nhiên, bạn vẫn có

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

Nhưng điều đó là bình thường đối với 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");

Nhưng miễn là bạn luôn luôn sử dụng constructdestructvà không có vị trí mới trên &data(), bạn vàng.


Nhờ @Deduplicator đã làm cho tôi nhận thức được sức mạnh của std::is_trivial:-)
Rumburak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.