Mam komponent, którego używam podczas implementowania typów ogólnych niskiego poziomu, które przechowują obiekt dowolnego typu (może być typem klasy lub nie), który może być pusty, aby skorzystać z pustej optymalizacji podstawowej :
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...>;
Ostatnio majstrowałem przy strukturach danych bez blokad i potrzebuję węzłów, które opcjonalnie zawierają dane na żywo. Po przydzieleniu węzły działają przez cały okres istnienia struktury danych, ale zawarte w niej dane są aktywne tylko wtedy, gdy węzeł jest aktywny, a nie wtedy, gdy węzeł znajduje się na wolnej liście. Zaimplementowałem węzły przy użyciu surowego przechowywania i umieszczania 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_;
};
co jest w porządku i eleganckie, ale marnuje porcję pamięci wielkości wskaźnika na węzeł, gdy T
jest pusty: jeden bajt dla raw_storage<T>::space_
i sizeof(std::atomic<list_node*>) - 1
bajty wypełnienia dla wyrównania. Byłoby miło skorzystać z EBO i przydzielić nieużywaną jednobajtową reprezentację na raw_container<T>
szczycie list_node::next_
.
Moja najlepsza próba stworzenia raw_ebo_storage
„ręcznego” 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)
));
}
};
który daje pożądane efekty:
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!");
ale także pewne niepożądane efekty, które zakładam z powodu naruszenia ścisłego aliasingu (3.10 / 10), chociaż znaczenie „dostępu do przechowywanej wartości obiektu” jest dyskusyjne dla pustego typu:
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.");
To rozwiązanie może również powodować niezdefiniowane zachowanie podczas budowy. W pewnym momencie program musi skonstruować obiekt Containee w surowym magazynie z umieszczeniem 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, "");
Przypomnij sobie, że pomimo tego, że jest pusty, kompletny obiekt musi mieć niezerowy rozmiar. Innymi słowy, pusty kompletny obiekt ma reprezentację wartości, która składa się z jednego lub więcej bajtów wypełniających.new
konstruuje kompletne obiekty, więc zgodna implementacja może ustawić te bajty wypełniające na dowolne wartości podczas konstruowania zamiast pozostawiać pamięć nietkniętą, jak w przypadku konstruowania pustego podstawowego podobiektu. Byłoby to oczywiście katastrofalne, gdyby te bajty wypełniające nakładały się na inne żywe obiekty.
Zatem pytanie brzmi, czy możliwe jest utworzenie klasy kontenera zgodnej ze standardami, która używa surowej pamięci / opóźnionej inicjalizacji dla zawartego obiektu i wykorzystuje EBO, aby uniknąć marnowania miejsca w pamięci na reprezentację zawartego obiektu?
? With maybe more tests on
Aby upewnić się, że jest skonstruowany próżniowo ... lub w jakiś sposób, aby zapewnić, że możesz konstruowaćT
bez konstruowaniaT
, zakładając, żeT::T()
ma skutki uboczne. Może klasa cech dla budowanych / niszczonych w sposób nieuzasadnionyT
która mówi, jak bezmyślnie skonstruowaćT
?space_
tablicy i bezpiecznie z niego korzystać, gdy nie jest on skonstruowany na liście wolnej? Wtedyspace_
nie będzie zawierał T, ale trochę opakowania wokół T i wskaźnika atomowego.Odpowiedzi:
Myślę, że sam udzieliłeś odpowiedzi w swoich różnych obserwacjach:
Wymagania te są sprzeczne. Odpowiedź brzmi więc nie , to nie jest możliwe.
Możesz jednak nieco bardziej zmienić swoje wymagania, wymagając zerowego narzutu tylko dla pustych, trywialnych typów.
Możesz zdefiniować nową cechę klasową, np
template <typename T> struct constructor_and_destructor_are_empty : std::false_type { };
Wtedy się specjalizujesz
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_; };
Następnie użyj tego w ten sposób:
using node = list_node<empty<char>>; static_assert(sizeof(node) == sizeof(std::atomic<node*>), "Good");
Oczywiście nadal masz
struct bar : raw_container<empty<char>> { empty<char> e; }; static_assert(sizeof(bar) == 1, "Yes, two objects sharing an address");
Ale to normalne w przypadku 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");
Ale tak długo, jak zawsze używać
construct
idestruct
a nie umieszczenie na nowy&data()
, jesteś złoty.źródło
std::is_trivial
:-)