Как я могу спроектировать хранилище, которое соответствует реализации стандарта std :: any?

В стандартном рабочем проекте (n4582, 20.6.3, p.552) изложено следующее предложение для реализации std::any:

Реализации должны избегать использования динамически выделяемой памяти для небольшого ограниченного объекта. [Пример: где построенный объект содержит только int. — конец примера] Такая оптимизация небольших объектов должна применяться только к типам T, для которых is_nothrow_move_constructible_v имеет значение true.

Насколько я знаю, std::any может быть легко реализовано через стирание типа / виртуальные функции и динамически распределенную память.

Как может std::any избегать динамического распределения и все еще уничтожать такие значения, если во время уничтожения не известна информация времени компиляции; Как будет разработано решение, которое следует предложению стандарта?


Если кто-то хочет увидеть возможную реализацию не динамической части, я разместил ее в Code Review: https://codereview.stackexchange.com/questions/128011/an-implementation-of-a-static-any-type

Это слишком долго для ответа здесь. Это основано на предложениях Kerrek SB на комментарии ниже.

8

Решение

Как std :: any может избежать динамического выделения и все еще уничтожить такое
значения, если информация о времени компиляции не известна во время
разрушение

Это похоже на загруженный вопрос. Последний проект требует этого конструктора:

template <class ValueType> any(ValueType &&value);

Я не могу понять, почему вам нужно иметь «стирание типа», если вы не хотите, чтобы код обрабатывал оба маленьких а также большие дела одновременно. Но тогда почему бы не иметь что-то подобное?1

template <typename T>
struct IsSmallObject : ...type_traits...

В первом случае у вас может быть указатель на ваше неинициализированное хранилище:

union storage
{
void* ptr;
typename std::aligned_storage<3 * sizeof(void*),
std::alignment_of<void*>::value>::type buffer;
};

Используя союз как @KerrekSB предложил.

Обратите внимание, что тип не должен быть известен для класса хранения. Используя какую-то систему обработки / отправки (не уверенную в истинном названии идиомы), система становится тривиальной на этом этапе.

Сначала рассмотрим, как будет выглядеть разрушение:

  template <typename T>
struct SmallHandler
{
// ...

static void destroy(any & bye)
{
T & value = *static_cast<T *>(static_cast<void*>(&bye.storage.buffer));
value.~T();
this.handle = nullptr;
}

// ...
};

Тогда any учебный класс:

// Note, we don't need to know `T` here!
class any
{
// ...

void clear() _NOEXCEPT
{
if (handle) this->call(destroy);
}

// ...
template <class>
friend struct SmallHandler;
};

Здесь мы выделяем логику, которая должна знать тип времени компиляции для системы обработчика / диспетчеризации, тогда как основная масса any класс должен иметь дело только с RTTI.


1Вот условия, которые я бы проверил:

  1. nothrow_move_constructible
  2. sizeof(T) <= sizeof(storage), В моем случае это 3 * sizeof(void*)
  3. alignof(T) <= alignof(storage), В моем случае это std::alignment_of<void*>::value
0

Другие решения

Как правило, any берет что-нибудь и динамически выделяет из него новый объект:

struct any {
placeholder* place;

template <class T>
any(T const& value) {
place = new holder<T>(value);
}

~any() {
delete place;
}
};

Мы используем тот факт, что placeholder полиморфно обрабатывать все наши операции — уничтожение, приведение и т. д. Но теперь мы хотим избежать выделения, что означает, что мы избегаем всех приятных вещей, которые дает нам полиморфизм, — и должны переопределить их. Для начала, у нас будет некоторый союз:

union Storage {
placeholder* ptr;
std::aligned_storage_t<sizeof(ptr), sizeof(ptr)> buffer;
};

где у нас есть некоторые template <class T> is_small_object { ... } решить, делаем ли мы ptr = new holder<T>(value) или же new (&buffer) T(value), Но конструирование — это не единственное, что мы должны сделать — мы также должны выполнить уничтожение и поиск информации о типе, которые выглядят по-разному в зависимости от того, в каком случае мы находимся. delete ptr или мы делаем static_cast<T*>(&buffer)->~T();последний из которых зависит от отслеживания T!

Итак, мы представляем нашу собственную vtable-подобную вещь. наш any затем будет держать на:

enum Op { OP_DESTROY, OP_TYPE_INFO };
void (*vtable)(Op, Storage&, const std::type_info* );
Storage storage;

Вместо этого вы могли бы создать новый указатель на функцию для каждой операции, но, возможно, здесь есть несколько других операций, которые я пропускаю (например, OP_CLONE, что может потребовать изменения передаваемого аргумента на union…) и ты не хочешь просто раздувать any размер с кучей функциональных указателей. Таким образом, мы теряем чуть-чуть производительности в обмен на большую разницу в размерах.

На строительстве мы затем заполняем оба storage и vtable:

template <class T,
class dT = std::decay_t<T>,
class V = VTable<dT>,
class = std::enable_if_t<!std::is_same<dT, any>::value>>
any(T&& value)
: vtable(V::vtable)
, storage(V::create(std::forward<T>(value))
{ }

где наш VTable типы что-то вроде:

template <class T>
struct PolymorphicVTable {
template <class U>
static Storage create(U&& value) {
Storage s;
s.ptr = new holder<T>(std::forward<U>(value));
return s;
}

static void vtable(Op op, Storage& storage, const std::type_info* ti) {
placeholder* p = storage.ptr;

switch (op) {
case OP_TYPE_INFO:
ti = &typeid(T);
break;
case OP_DESTROY:
delete p;
break;
}
}
};

template <class T>
struct InternalVTable {
template <class U>
static Storage create(U&& value) {
Storage s;
new (&s.buffer) T(std::forward<U>(value));
return s;
}

static void vtable(Op op, Storage& storage, const std::type_info* ti) {
auto p = static_cast<T*>(&storage.buffer);

switch (op) {
case OP_TYPE_INFO:
ti = &typeid(T);
break;
case OP_DESTROY:
p->~T();
break;
}
}
};

template <class T>
using VTable = std::conditional_t<sizeof(T) <= 8 && std::is_nothrow_move_constructible<T>::value,
InternalVTable<T>,
PolymorphicVTable<T>>;

а затем мы просто используем этот vtable для реализации наших различных операций. Подобно:

~any() {
vtable(OP_DESTROY, storage, nullptr);
}
0

Вдохновленный повысить любой Я придумал это (проверить это на ideone) (Я создал минимальный случай, чтобы показать, как уничтожить стертый контейнер типа any без динамической памяти. Я сосредоточился только на конструкторе / деструкторе, пропуская все остальное, игнорируя семантику перемещения и другие вещи)

#include <iostream>
#include <type_traits>

using std::cout;
using std::endl;

struct A { ~A() { cout << "~A" << endl; }};
struct B { ~B() { cout << "~B" << endl; }};

struct Base_holder {
virtual ~Base_holder() {}
};

template <class T>
struct Holder : Base_holder {
T value_;

Holder(T val) : value_{val} {}
};

struct Any {
std::aligned_storage_t<64> buffer_;
Base_holder* p_;

template <class T>
Any(T val)
{
p_ = new (&buffer_) Holder<T>{val};
}

~Any()
{
p_->~Base_holder();
}
};

auto main() -> int
{
Any a(A{});
Any b(B{});

cout << "--- Now we exit main ---" << endl;
}

Выход:

~A
~A
~B
~B
--- Now we exit main ---
~B
~A

Конечно, первый — это временные уничтожения, последние два доказывают, что уничтожение Any называет правильный деструктор.

Хитрость заключается в том, чтобы иметь полиморфизм. Вот почему мы имеем Base_holder а также Holder, Мы инициализируем их путем размещения новых в std::aligned_storage и мы явно называем деструктором.

Это просто для того, чтобы доказать, что вы можете вызвать правильный деструктор, не зная типа Any, Конечно, в реальной реализации у вас будет объединение для этого или указатель на динамически распределенную память и логическое значение, указывающее, какая у вас есть.

-1
По вопросам рекламы [email protected]