Какие варианты использования для "размещения новых"?
Кто-нибудь здесь когда-либо использовал "размещение нового" в C++? Если да, то для чего? Мне кажется, что это будет полезно только на оборудовании с отображенной памятью.
25 ответов
Размещение нового позволяет вам создать объект в памяти, который уже выделен.
Возможно, вы захотите сделать это для оптимизации, когда вам нужно создать несколько экземпляров объекта, и быстрее не перераспределять память каждый раз, когда вам нужен новый экземпляр. Вместо этого может быть более эффективно выполнить одно выделение для куска памяти, который может содержать несколько объектов, даже если вы не хотите использовать все это сразу.
DevX дает хороший пример:
Стандарт C++ также поддерживает размещение оператора new, который создает объект в заранее выделенном буфере. Это полезно при создании пула памяти, сборщика мусора или просто, когда производительность и безопасность исключений имеют первостепенное значение (нет опасности сбоя выделения, так как память уже выделена, а создание объекта в предварительно выделенном буфере занимает меньше времени):
char *buf = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi"); // placement new
string *q = new string("hi"); // ordinary heap allocation
Вы также можете быть уверены, что не может быть ошибки выделения в определенной части критического кода (например, в коде, выполняемом кардиостимулятором). В этом случае вы захотите выделить память раньше, а затем использовать новое размещение в критическом разделе.
Распределение в размещении новых
Вы не должны освобождать каждый объект, который использует буфер памяти. Вместо этого вы должны удалить [] только оригинальный буфер. Затем вам придется вызывать деструкторы ваших классов вручную. Хорошее предложение по этому вопросу см. В разделе часто задаваемых вопросов Страуструпа: " Есть ли удаление места размещения"?
Мы используем его с пользовательскими пулами памяти. Просто набросок:
class Pool {
public:
Pool() { /* implementation details irrelevant */ };
virtual ~Pool() { /* ditto */ };
virtual void *allocate(size_t);
virtual void deallocate(void *);
static Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};
class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };
// elsewhere...
void *pnew_new(size_t size)
{
return Pool::misc_pool()->allocate(size);
}
void *pnew_new(size_t size, Pool *pool_p)
{
if (!pool_p) {
return Pool::misc_pool()->allocate(size);
}
else {
return pool_p->allocate(size);
}
}
void pnew_delete(void *p)
{
Pool *hp = Pool::find_pool(p);
// note: if p == 0, then Pool::find_pool(p) will return 0.
if (hp) {
hp->deallocate(p);
}
}
// elsewhere...
class Obj {
public:
// misc ctors, dtors, etc.
// just a sampling of new/del operators
void *operator new(size_t s) { return pnew_new(s); }
void *operator new(size_t s, Pool *hp) { return pnew_new(s, hp); }
void operator delete(void *dp) { pnew_delete(dp); }
void operator delete(void *dp, Pool*) { pnew_delete(dp); }
void *operator new[](size_t s) { return pnew_new(s); }
void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
void operator delete[](void *dp) { pnew_delete(dp); }
void operator delete[](void *dp, Pool*) { pnew_delete(dp); }
};
// elsewhere...
ClusterPool *cp = new ClusterPool(arg1, arg2, ...);
Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);
Теперь вы можете кластеризовать объекты в одной области памяти, выбрать распределитель, который очень быстр, но не освобождает память, использовать отображение памяти и любую другую семантику, которую вы хотите наложить, выбрав пул и передав его в качестве аргумента для размещения объекта новый оператор.
Это полезно, если вы хотите отделить распределение от инициализации. STL использует размещение новых для создания элементов контейнера.
Я использовал это в программировании в реальном времени. Обычно мы не хотим выполнять какое-либо динамическое распределение (или освобождение) после запуска системы, потому что нет никакой гарантии, сколько времени это займет.
Что я могу сделать, так это предварительно выделить большой кусок памяти (достаточно большой, чтобы вместить любое количество того, что может потребоваться классу). Затем, как только я выясню во время выполнения, как создавать объекты, можно использовать новое размещение, чтобы создавать объекты именно там, где я их хочу. Я знаю, что использовал одну из них, чтобы помочь создать гетерогенный кольцевой буфер.
Это, конечно, не для слабонервных, но именно поэтому они делают синтаксис для этого довольно грубым.
Я использовал его для создания объектов, размещенных в стеке с помощью alloca().
бесстыдная вилка: я писал об этом здесь.
Фактически требуется реализовать любой тип структуры данных, которая выделяет больше памяти, чем минимально требуется для количества вставляемых элементов (т. Е. Все, кроме связанной структуры, которая выделяет один узел за раз).
Взять контейнеры как unordered_map
, vector
, или же deque
, Все они выделяют больше памяти, чем минимально требуется для вставленных вами элементов, чтобы избежать необходимости выделения кучи для каждой отдельной вставки. Давайте использовать vector
как самый простой пример.
Когда вы делаете:
vector<Foo> vec;
// Allocate memory for a thousand Foos:
vec.reserve(1000);
... это на самом деле не построить тысячу Фоос. Он просто выделяет / резервирует память для них. Если vector
не использовал размещение new здесь, это было бы конструкцией по умолчанию Foos
повсюду, а также необходимость вызывать их деструкторы даже для элементов, которые вы вообще никогда не вставляли.
Распределение!= Строительство, Освобождение!= Разрушение
Говоря в общем, для реализации многих структур данных, подобных описанным выше, вы не можете рассматривать выделение памяти и конструирование элементов как одну неделимую вещь, и вы также не можете рассматривать освобождение памяти и уничтожение элементов как одну неделимую вещь.
Должно быть разделение между этими идеями, чтобы избежать лишнего вызова конструкторов и деструкторов, излишне левых и правых, и именно поэтому стандартная библиотека разделяет идею std::allocator
(который не создает и не уничтожает элементы, когда он выделяет / освобождает память *) вдали от контейнеров, которые его используют, которые создают элементы вручную, используя размещение новых, и уничтожают элементы вручную, используя явные вызовы деструкторов.
- Я ненавижу дизайн
std::allocator
но это другая тема, о которой я не буду рассказывать.:-D
Так или иначе, я имею тенденцию использовать это много, так как я написал много стандартных C++ контейнеров общего назначения, которые не могли быть построены с точки зрения существующих. Среди них есть небольшая векторная реализация, которую я создал пару десятилетий назад, чтобы избежать выделения кучи в обычных случаях, и эффективная память (не выделяет один узел за раз). В обоих случаях я не смог реализовать их с использованием существующих контейнеров, поэтому мне пришлось использовать placement new
чтобы избежать лишнего вызова конструкторов и деструкторов на ненужные вещи слева и справа.
Естественно, если вы когда-нибудь работаете с пользовательскими распределителями, чтобы распределять объекты по отдельности, например, в виде свободного списка, тогда вам, как правило, также стоит использовать placement new
вот так (основной пример, который не заботится об исключительной безопасности или RAII):
Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);
Я использовал его для создания класса Variant (т. Е. Объекта, который может представлять одно значение, которое может быть одним из нескольких различных типов).
Если все типы значений, поддерживаемые классом Variant, являются типами POD (например, int, float, double, bool), тогда достаточно тегированного объединения в стиле C, но если вы хотите, чтобы некоторые типы значений были объектами C++ (например, std::string), функция объединения C не подойдет, так как типы данных, отличные от POD, не могут быть объявлены как часть объединения.
Поэтому вместо этого я выделяю байтовый массив, который достаточно большой (например, sizeof(the_largest_data_type_I_support)), и использую размещение new, чтобы инициализировать соответствующий объект C++ в этой области, когда в Variant установлено значение этого типа. (И, конечно, размещение удаляется заранее при переключении с другого типа данных, отличного от POD)
Главный Компьютерщик: БИНГО! Вы получили это полностью - это именно то, для чего это идеально. Во многих встроенных средах внешние ограничения и / или сценарий общего использования заставляют программиста отделить выделение объекта от его инициализации. С ++, объединившись, называет это "созданием экземпляра"; но всякий раз, когда действие конструктора должно быть явно вызвано БЕЗ динамического или автоматического выделения, размещение нового - способ сделать это. Это также идеальный способ найти глобальный объект C++, прикрепленный к адресу аппаратного компонента (ввод-вывод с отображением в памяти) или для любого статического объекта, который по какой-либо причине должен находиться по фиксированному адресу.
Я думаю, что это не было подчеркнуто ни одним ответом, но другой хороший пример и использование для нового размещения - уменьшить фрагментацию памяти (с помощью пулов памяти). Это особенно полезно во встроенных системах и системах с высокой доступностью. В этом последнем случае это особенно важно, потому что для системы, которая должна работать 24/365 дней, очень важно не иметь фрагментации. Эта проблема не имеет ничего общего с утечкой памяти.
Даже когда используется очень хорошая реализация malloc (или аналогичная функция управления памятью), очень трудно долго справляться с фрагментацией. В какой-то момент, если вы не будете умело управлять вызовами резервирования / освобождения памяти, вы можете получить множество небольших пробелов, которые трудно использовать повторно (назначить для новых резервирований). Таким образом, одним из решений, которые используются в этом случае, является использование пула памяти для выделения перед рукой памяти для объектов приложения. После этого каждый раз, когда вам нужна память для какого-либо объекта, вы просто используете новое размещение, чтобы создать новый объект в уже зарезервированной памяти.
Таким образом, после запуска приложения у вас уже есть вся необходимая память. Все новое резервирование / освобождение памяти направляется в выделенные пулы (у вас может быть несколько пулов, по одному для каждого отдельного класса объектов). В этом случае не происходит фрагментации памяти, поскольку в ней нет пробелов, и ваша система может работать в течение очень длительных периодов (лет), не страдая от фрагментации.
Я видел это на практике специально для ОСРВ VxWorks, поскольку ее система выделения памяти по умолчанию сильно страдает от фрагментации. Поэтому выделение памяти с помощью стандартного метода new/malloc в проекте было в основном запрещено. Все резервирования памяти должны идти в выделенный пул памяти.
Размещение new также очень полезно при сериализации (скажем, с boost::serialization). За 10 лет C++ это только второй случай, для которого мне нужно было разместить новое (третий, если вы включаете интервью:)).
Это также полезно, когда вы хотите повторно инициализировать глобальные или статически распределенные структуры.
Старый способ C использовал memset()
установить все элементы в 0. Вы не можете сделать это в C++ из-за vtables и пользовательских конструкторов объектов.
Поэтому я иногда использую следующее
static Mystruct m;
for(...) {
// re-initialize the structure. Note the use of placement new
// and the extra parenthesis after Mystruct to force initialization.
new (&m) Mystruct();
// do-some work that modifies m's content.
}
Это полезно, если вы собираете ядро - куда вы помещаете код ядра, который вы читаете с диска или из таблицы? Вы должны знать, где прыгать.
Или в других, очень редких обстоятельствах, например, когда у вас много выделенного помещения и вы хотите разместить несколько конструкций друг за другом. Они могут быть упакованы таким образом без необходимости использования оператора offsetof(). Однако для этого есть и другие приемы.
Я также считаю, что некоторые реализации STL используют размещение новых, таких как std::vector. Таким образом, они выделяют место для 2^n элементов и не всегда должны перераспределяться.
Используется std::vector<>
так как std::vector<>
как правило, выделяет больше памяти, чем есть objects
в vector<>
,
Я использовал его для хранения объектов с отображенными в память файлами.
Конкретным примером была база данных изображений, которая обрабатывала большое количество больших изображений (больше, чем могло поместиться в памяти).
Я видел, как это использовалось в качестве небольшого снижения производительности для указателя "динамического типа" (в разделе "Под капотом"):
Но вот хитрость, которую я использовал, чтобы получить быструю производительность для небольших типов: если удерживаемое значение может поместиться в пустоте *, я на самом деле не беспокоюсь о выделении нового объекта, я принудительно помещаю его в сам указатель с помощью размещения new,
Я использовал его для создания объектов на основе памяти, содержащей сообщения, полученные из сети.
Это может быть удобно при использовании разделяемой памяти, среди прочего... Например: http://www.boost.org/doc/libs/1_51_0/doc/html/interprocess/synchronization_mechanisms.html
Как правило, размещение нового используется, чтобы избавиться от стоимости размещения "нормального нового".
Другой сценарий, в котором я использовал это место, где я хотел получить доступ к указателю на объект, который еще должен был быть создан, для реализации отдельного документа на документ.
Механизмы сценариев могут использовать его в собственном интерфейсе для выделения собственных объектов из сценариев. См. Angelscript (www.angelcode.com/angelscript) для примеров.
Единственное место, где я столкнулся с ним, - это контейнеры, которые выделяют непрерывный буфер и затем заполняют его объектами по мере необходимости. Как уже упоминалось, std::vector может сделать это, и я знаю, что некоторые версии MFC CArray и / или CList делали это (потому что именно там я впервые столкнулся с этим). Метод перераспределения буфера является очень полезной оптимизацией, и размещение new является практически единственным способом создания объектов в этом сценарии. Это также иногда используется для создания объектов в блоках памяти, размещенных вне вашего прямого кода.
Я использовал его в аналогичном качестве, хотя и не часто. Это полезный инструмент для набора инструментов C++.
См. Файл fp.h в проекте xll по адресу http://xll.codeplex.com/ Он решает проблему "необоснованной болтовни с компилятором" для массивов, которым нравится носить с собой свои размеры.
typedef struct _FP { короткие строки без знака; беззнаковые короткие int столбцы; двойной массив [1]; /* На самом деле, массив [строки][столбцы] */ } FP;
Вот убийственное использование для конструктора на месте C++: выравнивание по строке кэша, а также другие полномочия двух границ. Вот мой сверхбыстрый алгоритм выравнивания указателя для любой степени 2 границ с 5 или менее однократными инструкциями:
/* Quickly aligns the given pointer to a power of two boundary IN BYTES.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param boundary_byte_count The boundary byte count that must be an even
power of 2.
@warning Function does not check if the boundary is a power of 2! */
template <typename T = char>
inline T* AlignUp(void* pointer, uintptr_t boundary_byte_count) {
uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
value += (((~value) + 1) & (boundary_byte_count - 1));
return reinterpret_cast<T*>(value);
}
struct Foo { Foo () {} };
char buffer[sizeof (Foo) + 64];
Foo* foo = new (AlignUp<Foo> (buffer, 64)) Foo ();
Теперь разве это не вызывает улыбку на лице (:-). Я ♥♥♥ C++1x
У меня тоже есть идея. В C++ действует принцип нулевых накладных расходов . Но исключения не следуют этому принципу, поэтому иногда они отключаются переключателем компилятора.
Давайте посмотрим на этот пример:
#include <new>
#include <cstdio>
#include <cstdlib>
int main() {
struct A {
A() {
printf("A()\n");
}
~A() {
printf("~A()\n");
}
char data[1000000000000000000] = {}; // some very big number
};
try {
A *result = new A();
printf("new passed: %p\n", result);
delete result;
} catch (std::bad_alloc) {
printf("new failed\n");
}
}
Мы размещаем здесь большую структуру, проверяем, успешно ли выделено, и удаляем ее.
Но если у нас отключены исключения, мы не сможем использовать блок try и не сможем обработать ошибку new [].
Итак, как мы можем это сделать? Вот как:
#include <new>
#include <cstdio>
#include <cstdlib>
int main() {
struct A {
A() {
printf("A()\n");
}
~A() {
printf("~A()\n");
}
char data[1000000000000000000] = {}; // some very big number
};
void *buf = malloc(sizeof(A));
if (buf != nullptr) {
A *result = new(buf) A();
printf("new passed: %p\n", result);
result->~A();
free(result);
} else {
printf("new failed\n");
}
}
- Используйте простой malloc
- Проверьте, не удалось ли это способом C
- В случае успеха мы используем новое размещение.
- Вызов деструктора вручную (мы не можем просто вызвать удаление)
- Звоните бесплатно, потому что мы вызвали malloc
UPD Useless написал комментарий, который открыл мне, что существует новый (nothrow), который следует использовать в этом случае, но не метод, который я написал ранее. Пожалуйста, не используйте код, который я написал ранее. Простите.
У меня есть еще одна идея (она актуальна для C++11).
Давайте посмотрим на следующий пример:
#include <cstddef>
#include <cstdio>
int main() {
struct alignas(0x1000) A {
char data[0x1000];
};
printf("max_align_t: %zu\n", alignof(max_align_t));
A a;
printf("a: %p\n", &a);
A *ptr = new A;
printf("ptr: %p\n", ptr);
delete ptr;
}
В стандарте C++ 11 GCC дает следующий результат:
max_align_t: 16
a: 0x7ffd45e6f000
ptr: 0x1fe3ec0
не выровнен должным образом.
В стандарте C++ 17 и последующих версиях GCC дает следующий результат:
max_align_t: 16
a: 0x7ffc924f6000
ptr: 0x9f6000
выровнен правильно.
Насколько мне известно, стандарт C++ не поддерживал новое выравнивание до того, как появился C++ 17, и если ваша структура имеет выравнивание больше, чем
max_align_t
, у вас могут быть проблемы. Чтобы обойти эту проблему в C++ 11, вы можете использовать
aligned_alloc
.
#include <cstddef>
#include <cstdlib>
#include <cstdio>
#include <new>
int main() {
struct alignas(0x1000) A {
char data[0x1000];
};
printf("max_align_t: %zu\n", alignof(max_align_t));
A a;
printf("a: %p\n", &a);
void *buf = aligned_alloc(alignof(A), sizeof(A));
if (buf == nullptr) {
printf("aligned_alloc() failed\n");
exit(1);
}
A *ptr = new(buf) A();
printf("ptr: %p\n", ptr);
ptr->~A();
free(ptr);
}
ptr
будет выровнен в этом случае.
max_align_t: 16
a: 0x7ffe56b57000
ptr: 0x2416000
Кто-нибудь здесь когда-либо использовал C++ "новое размещение"? Если да, то зачем? Мне кажется, что это будет полезно только на оборудовании с отображением памяти.
Это действительно полезно, когда нужно скопировать (передать как выходные данные):
- не копируемые объекты (например, где
operator=()
был автоматически удален, поскольку класс содержитconst
член) ИЛИ - нетривиально копируемые объекты (при использовании
memcpy()
неопределенное поведение)
... изнутри функции.
Это (получение этих не копируемых или нетривиально копируемых объектов из функции) может помочь в модульном тестировании этой функции, позволяя вам увидеть, что определенный объект данных теперь выглядит определенным образом после обработки этой функцией, ИЛИ это может просто быть частью вашего обычного API для любого использования, которое вы сочтете нужным. Давайте рассмотрим эти примеры и подробно объясним, что я имею в виду и как можно использовать "новое размещение" для решения этих проблем.
TL; DR;
Примечание. В этом ответе я протестировал каждую строчку кода. Оно работает. Это действительно так. Это не нарушает стандарт C++.
Новое место размещения:
- Замена в C++ на
=
когдаoperator=()
(оператор присваивания) удаляется, и вам необходимо "скопировать" (фактически скопировать) объект, который в противном случае не может быть скопирован, в заданную ячейку памяти. - Замена в C++ на
memcpy()
когда ваш объект не может быть легко скопирован, что означает, что использованиеmemcpy()
для копирования этого нетривиально копируемого объекта "может быть неопределенным".
Важно: "не копируемый" объект НЕ является действительно некопируемым. Его просто невозможно скопировать через
=
оператор - это все, что является вызовом базового класса
operator=()
функция перегрузки. Это означает, что когда вы это сделаете
B = C;
, то, что на самом деле происходит, - это вызов
B.operator=(C);
, и когда вы это сделаете
A = B = C;
, что на самом деле происходит
A.operator=(B.operator=(C));
. Следовательно, "не копируемые" объекты можно копировать только другими способами, например, через конструктор копирования класса, поскольку, опять же, класс не имеет
operator=()
метод. "Новое размещение" можно использовать для вызова любого из множества конструкторов, которые могут существовать в классе, для создания объекта в желаемой предварительно выделенной области памяти. Поскольку синтаксис "нового размещения" позволяет вызывать любой конструктор в классе, это включает в себя передачу ему существующего экземпляра класса для размещения нового вызова конструктора копирования класса. для копирования-создания нового объекта из переданного объекта в другое место в памяти. Копирование-создание одного объекта в другое место в памяти... это копия. Это действие создает копию этого исходного объекта. Когда это будет сделано, вы МОЖЕТЕ иметь два объекта (экземпляра), которые идентичны байтам, буквально байт за байтом (в зависимости от реализации вашего конструктора копирования), расположенных в двух разных местах в памяти. Это по определению копия. Это просто не было сделано с использованием класса
operator=()
метод есть все.
Следовательно, можно определить класс как "не копируемый", если у него нет
operator=()
method, but it is still very-much copyable, legally, according to the C++ standard and mechanisms provided by C++, safely and without undefined behavior, using its copy constructor and placement new syntax, as demonstrated below.
Reminder: all lines of code below work. You can run much of the code right here, incl. many of the code blocks below, although it may require some commenting/uncommenting blocks of code since it is not cleanly set up into separate examples.
1. What is a non-copyable object?
A non-copyable object cannot be copied with the
=
operator (operator=()
function). That's it! It can still be legally copied, however. See the really important note just above.
Non-copyable class Example 1:
Здесь копирующее построение нормально, но копирование запрещено, поскольку мы явно удалили оператор присваивания. Пытаюсь сделать
nc2 = nc1;
приводит к этой ошибке времени компиляции:
error: use of deleted function ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’
Вот полный пример:
#include <stdio.h>
class NonCopyable1
{
public:
int i = 5;
// Delete the assignment operator to make this class non-copyable
NonCopyable1& operator=(const NonCopyable1& other) = delete;
};
int main()
{
printf("Hello World\n");
NonCopyable1 nc1;
NonCopyable1 nc2;
nc2 = nc1; // copy assignment; compile-time error!
NonCopyable1 nc3 = nc1; // copy constructor; works fine!
return 0;
}
Некопируемый класс Пример 2:
Здесь копирующее построение нормально, но копирование запрещено, поскольку класс содержит
const
member, в который нельзя записать (предположительно, поскольку, очевидно, есть обходные пути). Пытаюсь сделать
nc2 = nc1;
приводит к этой ошибке времени компиляции:
error: use of deleted function ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’ note: ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’ is implicitly deleted because the default definition would be ill-formed: error: non-static const member ‘const int NonCopyable1::i’, can’t use default assignment operator
Полный пример:
#include <stdio.h>
class NonCopyable1
{
public:
const int i = 5; // classes with `const` members are non-copyable by default
};
int main()
{
printf("Hello World\n");
NonCopyable1 nc1;
NonCopyable1 nc2;
nc2 = nc1; // copy assignment; compile-time error!
NonCopyable1 nc3 = nc1; // copy constructor; works fine!
return 0;
}
Итак, если класс нельзя копировать, вы НЕ можете делать следующее, чтобы получить его копию в качестве вывода! Линия
outputData = data;
вызовет сбой компиляции с предыдущими сообщениями об ошибках, показанными в последнем примере чуть выше!
#include <functional>
#include <stdio.h>
class NonCopyable1
{
public:
const int i; // classes with `const` members are non-copyable by default
// Constructor to custom-initialize `i`
NonCopyable1(int val = 5) : i(val)
{
// nothing else to do
}
};
// Some class which (perhaps asynchronously) processes data. You attach a
// callback, which gets called later.
// - Also, this maybe a shared library over which you have no or little
// control, so you cannot easily change the prototype of the callable/callback
// function.
class ProcessData
{
public:
void attachCallback(std::function<void(void)> callable)
{
callback_ = callable;
}
void callCallback()
{
callback_();
}
private:
std::function<void(void)> callback_;
};
int main()
{
printf("Hello World\n");
NonCopyable1 outputData; // we need to receive back data through this object
printf("outputData.i (before) = %i\n", outputData.i); // is 5
ProcessData processData;
// Attach a lambda function as a callback, capturing `outputData` by
// reference so we can receive back the data from inside the callback via
// this object even though the callable prototype returns `void` (is a
// `void(void)` callable/function).
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// NOT ALLOWED SINCE COPY OPERATOR (Assignment operator) WAS
// AUTO-DELETED since the class has a `const` data member!
outputData = data;
});
processData.callCallback();
// verify we get 999 here, NOT 5!
printf("outputData.i (after) = %i\n", outputData.i);
return 0;
}
Одно решение: запишите данные в
outputData
. Это вполне приемлемо в C, но не всегда нормально в C++.
Cppreference.com заявляет (выделено мной):
Если объекты потенциально перекрываются или не являются TriviallyCopyable, поведение memcpy не указано и может быть неопределенным.
и:
Примечания
Объекты тривиально копируемых типов, которые не являются потенциально перекрывающимися подобъектами, являются единственными объектами C++, которые можно безопасно копировать с помощьюstd::memcpy
или сериализованы в / из двоичных файлов сstd::ofstream::write()
/std::ifstream::read()
.
( https://en.cppreference.com/w/cpp/string/byte/memcpy)
Итак, давайте просто будем осторожны и убедитесь, что объект легко копируется, прежде чем копировать его с помощью
memcpy()
. Замените эту часть выше:
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// NOT ALLOWED SINCE COPY OPERATOR (Assignment operator) WAS
// AUTO-DELETED since the class has a `const` data member!
outputData = data;
});
с этим. Обратите внимание на использование
memcpy()
на этот раз скопировать данные и
std::is_trivially_copyable
чтобы гарантировать, что во время компиляции этот тип действительно безопасно копировать с
memcpy()
!:
// (added to top)
#include <cstring> // for `memcpy()`
#include <type_traits> // for `std::is_trivially_copyable<>()`
// Attach a lambda function as a callback, capturing `outputData` by
// reference so we can receive back the data from inside the callback via
// this object even though the callable prototype returns `void` (is a
// `void(void)` callable/function).
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
memcpy(&outputData, &data, sizeof(data));
});
Пример вывода программы теперь, когда ее можно компилировать и запускать. Оно работает!
Hello World outputData.i (before) = 5 outputData.i (after) = 999
Однако для большей безопасности вы должны вручную вызвать деструктор перезаписываемого объекта перед его перезаписью, например:
ЛУЧШЕЕ РЕШЕНИЕ MEMCPY():
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
memcpy(&outputData, &data, sizeof(data));
});
Если
static_assert()
выше не работает, однако вам не следует использовать
memcpy()
. Всегда безопасный и лучший вариант C++, поэтому, является использование "размещение новых".
Здесь мы просто копируем-конструируем
data
прямо в область памяти, занятую
outputData
. Вот что делает для нас синтаксис "нового размещения"! Он НЕ выделяет память динамически, как
new
оператор обычно делает. Обычно new
Оператор сначала динамически выделяет память в куче, а затем создает объект в этой памяти, вызывая конструктор объекта. Однако размещение new НЕ выполняет часть распределения. Вместо этого он просто пропускает эту часть и создает объект в памяти по указанному вами адресу! ВЫ должны быть тем, кто заранее выделяет эту память, статически или динамически, и ВЫ должны убедиться, что память правильно выровнена для этого объекта (см. alignof
и alignas
и новый пример размещения здесь) (он будет в этом случае, поскольку мы явно создали
outputData
объект как объект, вызывая его конструктор с
NonCopyable1 outputData;
), и ВЫ должны убедиться, что буфер / пул памяти достаточно велик для хранения данных, которые вы собираетесь в него встроить.
Итак, общий синтаксис нового размещения следующий:
// Call`T`'s specified constructor below, constructing it as an object right into
// the memory location pointed to by `ptr_to_buffer`. No dynamic memory allocation
// whatsoever happens at this time. The object `T` is simply constructed into this
// address in memory.
T* ptr_to_T = new(ptr_to_buffer) T(optional_input_args_to_T's_constructor);
В нашем случае это будет выглядеть так, вызывая конструктор копирования объекта
NonCopyable1
класс, который мы уже неоднократно доказали выше, действителен даже при удалении оператора присваивания / копирования:
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
Наш последний
attachCallback
лямбда теперь выглядит так, с новым синтаксисом размещения вместо
memcpy()
. Обратите внимание, что проверка того, что объект легко копируется, больше не требуется.
===> ЛУЧШЕЕ РЕШЕНИЕ C++ ВО ВСЕМ МИРЕ - ИЗБЕГАЕТ ВСПОМОГАТЕЛЬНОСТИ ПОСРЕДСТВОМ КОПИРОВАНИЯ - СОЗДАНИЕ НЕПОСРЕДСТВЕННО В ЦЕЛЕВОЕ МЕСТОПОЛОЖЕНИЕ ПАМЯТИ ИСПОЛЬЗУЯ РАЗМЕЩЕНИЕ НОВИНКА: <==== ИСПОЛЬЗУЙТЕ ЭТО! ====
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
// Assume that `data` will be further manipulated and used below now, but we needed
// its state at this moment in time.
// Note also that under the most trivial of cases, we could have also just called
// out custom constructor right here too, like this. You can call whatever
// constructor you want!
// new(&outputData) NonCopyable1(999);
// ...
});
2. Что такое нетривиально копируемый объект?
Нетривиально копируемый объект может быть объектом, который содержит виртуальные методы и вещи, так как это может привести к тому, что класс должен будет отслеживать "vee-указатели" (vptr
) и "vee столы" (vtbl
s), чтобы указать на правильную виртуальную реализацию в памяти. Подробнее об этом читайте здесь: "Схема хранения полиморфных объектов" доктора Добба. Однако даже в этом случае, пока вы
memcpy()
переходя от одного и того же процесса к одному и тому же процессу (то есть: в одном и том же пространстве виртуальной памяти), а НЕ между процессами и НЕ десериализация с диска в ОЗУ, мне кажется, что
memcpy()
технически будет работать нормально и не вызывать ошибок (и я доказал это на нескольких примерах для себя), но технически кажется, что это поведение, не определенное стандартом C++, поэтому это поведение undefined, поэтому оно нельзя полагаться на 100% от компилятора к компилятору и от одной версии C++ к другой, поэтому... это неопределенное поведение, и вы не должны
memcpy()
в этом случае.
Другими словами, если
static_assert(std::is_trivially_copyable<NonCopyable1>::value);
выше не удалось проверить, НЕ используйте
memcpy()
. Вместо этого вы должны использовать "новое размещение"!
Один из способов добиться сбоя статического утверждения - просто объявить или определить пользовательский оператор копирования / присваивания в определении вашего класса для вашего
NonCopyable1
класс, например:
// Custom copy/assignment operator declaration:
NonCopyable1& operator=(const NonCopyable1& other);
// OR:
// Custom copy/assignment operator definition:
NonCopyable1& operator=(const NonCopyable1& other)
{
// Check for, **and don't allow**, self assignment!
// ie: only copy the contents from the other object
// to this object if it is not the same object (ie: if it is not
// self-assignment)!
if(this != &other)
{
// copy all non-const members manually here, if the class had any; ex:
// j = other.j;
// k = other.k;
// etc.
// Do deep copy of data via any member **pointers**, if such members exist
}
// the assignment function (`operator=()`) expects you to return the
// contents of your own object (the left side), passed by reference, so
// that constructs such as `test1 = test2 = test3;` are valid!
// See this reference, from Stanford, p11, here!:
// http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf
// MyClass one, two, three;
// three = two = one;
return *this;
}
(Дополнительные примеры настраиваемых конструкторов копирования, операторов присваивания и т.д., а также "Правило трех" и "Правило пяти" см. В моем репозитории hello world и пример здесь.)
Итак, теперь, когда у нас есть настраиваемый оператор присваивания, класс больше нельзя просто копировать, и этот код:
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
memcpy(&outputData, &data, sizeof(data));
});
выдаст эту ошибку:
main.cpp: In lambda function: main.cpp:151:13: error: static assertion failed: NonCopyable1 must be a trivially-copyable type in order to guarantee that `memcpy()` is safe to use on it. static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must " ^~~~~~~~~~~~~
Итак, вы ДОЛЖНЫ /(действительно должны) использовать вместо этого "новое размещение", как это ранее описано выше:
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
});
Подробнее о предварительном выделении пула буфера / памяти для использования с "новым размещением"
Если вы действительно просто собираетесь использовать новое размещение для копирования-конструкции прямо в пул памяти / общую память / предварительно выделенное пространство объектов, в любом случае нет необходимости использовать
NonCopyable1 outputData;
чтобы создать в этой памяти бесполезный экземпляр, который мы все равно должны уничтожить позже. Вместо этого вы можете просто использовать байтовый пул памяти. Формат такой:
(Из раздела "Новое размещение" здесь: https://en.cppreference.com/w/cpp/language/new)
// within any scope...
{
char buf[sizeof(T)]; // Statically allocate memory large enough for any object of
// type `T`; it maybe misaligned!
// OR, to force proper alignment of your memorybuffer for your object of type `T`,
// you may specify memory alignment with `alignas()` like this instead:
alignas(alignof(T)) char buf[sizeof(T)];
T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your
// pre-allocated storage at memory address `buf`.
tptr->~T(); // You must **manually** call the object's destructor.
} // Leaving scope here auto-deallocates your statically-allocated
// memory `buf`.
Итак, в моем примере выше этот статически выделенный выходной буфер:
// This constructs an actual object here, calling the `NonCopyable1` class's
// default constructor.
NonCopyable1 outputData;
станет таким:
// This is just a statically-allocated memory pool. No constructor is called.
// Statically allocate an output buffer properly aligned, and large enough,
// to store 1 single `NonCopyable1` object.
alignas(alignof(NonCopyable1)) uint8_t outputData[sizeof(NonCopyable1)];
NonCopyable1* outputDataPtr = (NonCopyable1*)(&outputData[0]);
а затем вы прочитаете содержание
outputData
объект через
outputDataPtr
указатель.
Первый способ (NonCopyable1 outputData;
) лучше всего, если существует конструктор для этого класса, который не требует входных параметров, к которым у вас НЕТ доступа во время создания этого буфера, и если вы намереваетесь сохранить только этот тип данных в этом буфере, тогда как последний
uint8_t
буферный метод лучше всего, если вы: A) НЕ имеете доступа ко всем входным параметрам, необходимым даже для создания объекта в том месте, где вам нужно создать этот буфер, ИЛИ B) если вы планируете хранить несколько типов данных в этом пуле памяти, возможно, для связи между потоками, модулями, процессами и т. д. в виде объединения.
Подробнее о C++ и о том, почему в данном случае он заставляет нас преодолевать эти трудности
Итак, вся эта "новая" вещь в C++ и необходимость в ней потребовали от меня большого изучения и долгого времени, чтобы осмыслить это. Поразмыслив над этим, мне пришло в голову, что парадигма C (откуда я родом) состоит в том, чтобы вручную выделить часть памяти, а затем вставить в нее что-то. Они предназначены для отдельных действий при работе как со статическим, так и с динамическим распределением памяти (помните: вы даже не можете установить значения по умолчанию для
struct
с!). Нет концепции конструктора или деструктора, и даже получение поведения деструктора на основе области видимости, который автоматически вызывается, когда переменная выходит из заданной области, является головной болью и требует некоторого причудливого расширения gcc
__attribute__((__cleanup__(my_variable)))
магия, как я демонстрирую в своем ответе здесь. Однако произвольное копирование с одного объекта на другой очень просто. Просто скопируйте объекты вокруг! Это контрастирует с парадигмой C++, которая представляет собой RAII (получение ресурсов - это инициализация). Эта парадигма фокусируется на объектах, готовых к использованию в момент их создания. Для этого они полагаются на конструкторы и деструкторы. Это означает, что создание такого объекта:
NonCopyable1 data(someRandomData);
, не просто выделяет память для этого объекта, он также вызывает конструктор объекта и конструирует (помещает) этот объект прямо в эту память. Он пытается сделать несколько вещей одновременно. Итак, в C++
memcpy()
и оператор присваивания (=
; AKA:
operator=()
function) явно более ограничены природой C++. Вот почему мы должны пройти через обручи этого странного процесса "скопировать-построить мой объект в заданное место памяти с помощью нового" процесса в C++ вместо того, чтобы просто создавать переменную и копировать в нее что-то позже, или
memcpy()
добавление чего-либо в него позже, если он содержит
const
member, как и в случае с C. C++ действительно пытается обеспечить соблюдение RAII, и отчасти они это делают.
Вы можете использовать
std::optional<>::emplace()
вместо
Начиная с C++17 вы можете использовать std::optional<>
как обертку для этого тоже. Современный C++
emplace()
функции различных контейнеров и оболочек делают то, что мы делали вручную выше с "новым размещением" (см. также мой ответ здесь и цитату о том, как
std::vector<T,Allocator>::emplace_back
"обычно использует новое размещение для создания элемента на месте").
std::optional
статически выделяет буфер, достаточно большой для объекта, который вы хотите в него поместить. Затем он либо сохраняет этот объект, либо
std::nullopt
(такой же как
{}
), что означает, что он не удерживает этот объект. Чтобы заменить один объект в нем другим, просто вызовите
emplace()
метод на
std::optional
объект. Это делает следующее:
Создает содержащееся значение на месте. Если
*this
уже содержит значение перед вызовом, содержащееся значение уничтожается путем вызова его деструктора.
Итак, сначала он вручную вызывает деструктор на существующем объекте, уже находящемся внутри него, если существующий объект уже находится внутри него, тогда он выполняет эквивалент "размещения нового" для копирования-конструирования нового объекта (который вы предоставляете) в этот пространство памяти.
Итак, этот выходной буфер:
NonCopyable1 outputData;
// OR
alignas(alignof(NonCopyable1)) uint8_t outputData[sizeof(NonCopyable1)];
NonCopyable1* outputDataPtr = (NonCopyable1*)(&outputData[0]);
теперь становится это:
# include <optional>
std::optional<NonCopyable1> outputData = std::nullopt;
и это "размещение новой" конструкции копирования в этот выходной буфер:
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
});
теперь становится этим
emplace()
добавление новых данных в этот буфер. Обратите внимание, что ручной вызов деструктора больше не нужен, поскольку
std::optional<>::emplace()
уже обрабатывает вызов деструктора для любого уже существующего объекта за нас!:
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// emplace `data` right into the `outputData` object
outputData.emplace(data);
});
Теперь, чтобы получить данные из
outputData
, просто разыменуйте его с помощью
*
, или позвоните по телефону
.value()
в теме. Пример:
// verify we get 999 here!
if (outputData.has_value())
{
printf("(*outputData).i (after) = %i\n", (*outputData).i);
// OR
printf("outputData.value().i (after) = %i\n", outputData.value().i);
}
else
{
printf("outputData.has_value() is false!");
}
Пример вывода:
Hello World (*outputData).i (after) = 999 outputData.value().i (after) = 999
Запустите этот полный пример кода здесь.
Ссылки и дополнительное, ОТЛИЧНОЕ чтение:
- *****+[некоторые из самых полезных и простых примеров "нового размещения", которые я когда-либо видел!] https://www.geeksforgeeks.org/placement-new-operator-cpp/
- [отличный пример] https://en.cppreference.com/w/cpp/language/new -> см. раздел "Новое размещение" и пример здесь! (Я помог написать пример).
- Как сделать этот объект C++ не копируемым?
- [подчеркивает действительно важный момент, что вызов новой строки размещения вызывает конструктор объекта по мере его создания!: Строка #3 (
Fred* f = new(place) Fred();
) по сути просто вызывает конструкторFred::Fred()
. Это означает "this
указатель вFred
конструктор будет равенplace
".] http://www.cs.technion.ac.il/users/yechiel/c++-faq/placement-new.html - "Схема хранения полиморфных объектов" доктора Добба.
- [хорошее введение до C++11 в "Правило трех" C++] http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf
- Мой пример и репозиторий "hello world", демонстрация пользовательских конструкторов копирования, операторов присваивания и т. Д., Относящихся к "Правилу трех" / "Правилу пяти" / "Правилу нуля" / "Правилу 0/3/5 в C++" ": https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/cpp/copy_constructor_and_assignment_operator/copy_constructor_and_assignment_operator.cpp
- [ отличная статья Microsoft об использовании C++17
std::optional<>
тип] https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/ - [связанный, поскольку "новое размещение" очень ясно решает и эту проблему, поскольку эта проблема была ключевым моментом и движущей силой большинства моих решений и примеров здесь!] Член const и оператор присваивания. Как избежать неопределенного поведения?