Можно ли действительно использовать размещение new в переносимом коде при его использовании для массивов?
Похоже, что указатель, который вы получаете от new [], не всегда совпадает с адресом, который вы передаете (5.3.4, примечание 12 в стандарте, похоже, подтверждает, что это правильно), но я не понимаю, как вы может выделить буфер для массива, если это так.
В следующем примере показана проблема. Этот пример, скомпилированный с помощью Visual Studio, приводит к повреждению памяти:
#include <new>
#include <stdio.h>
class A
{
public:
A() : data(0) {}
virtual ~A() {}
int data;
};
int main()
{
const int NUMELEMENTS=20;
char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
A *pA = new(pBuffer) A[NUMELEMENTS];
// With VC++, pA will be four bytes higher than pBuffer
printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);
// Debug runtime will assert here due to heap corruption
delete[] pBuffer;
return 0;
}
Глядя на память, кажется, что компилятор использует первые четыре байта буфера для хранения количества элементов в нем. Это означает, что, поскольку размер буфера составляет только sizeof(A)*NUMELEMENTS, последний элемент массива записывается в нераспределенную кучу.
Итак, вопрос в том, сможете ли вы узнать, сколько дополнительных накладных расходов требует ваша реализация, чтобы безопасно использовать размещение new []? В идеале мне нужна техника, переносимая между разными компиляторами. Обратите внимание, что, по крайней мере, в случае VC накладные расходы, похоже, различаются для разных классов. Например, если я удалю виртуальный деструктор в примере, адрес, возвращаемый из new [], будет таким же, как адрес, который я передал.
Хм ... если накладные расходы исчезнут при удалении виртуального деструктора, это может означать, что накладные расходы, скорее всего, связаны либо с vtable класса, либо с реализацией RTTI VStudio.
Или, по крайней мере, часть накладных расходов. Также возможно, что накладные расходы используются только в том случае, если в классе есть нетривиальный деструктор.





Я думаю, что gcc делает то же самое, что и MSVC, но, конечно, это не делает его «переносимым».
Я думаю, вы можете обойти проблему, когда NUMELEMENTS действительно является постоянной времени компиляции, например:
typedef A Arr[NUMELEMENTS];
A * p = новый (буфер) Arr;
Это должно использовать скалярное размещение new.
Это не так. operator new() против operator new[]() зависит от того, задействован ли тип массива, а не от того, был ли [] в исходном коде.
Лично я бы предпочел не использовать размещение new в массиве, а вместо этого использовать размещение new для каждого элемента в массиве индивидуально. Например:
int main(int argc, char* argv[])
{
const int NUMELEMENTS=20;
char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
A *pA = (A*)pBuffer;
for(int i = 0; i < NUMELEMENTS; ++i)
{
pA[i] = new (pA + i) A();
}
printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);
// dont forget to destroy!
for(int i = 0; i < NUMELEMENTS; ++i)
{
pA[i].~A();
}
delete[] pBuffer;
return 0;
}
Независимо от метода, который вы используете, убедитесь, что вы вручную уничтожили каждый из этих элементов в массиве, прежде чем удалять pBuffer, так как вы можете получить утечки;)
Примечание: Я не скомпилировал это, но думаю, что это должно работать (я нахожусь на машине, на которой не установлен компилятор C++). Это все еще указывает на точку :) Надеюсь, это поможет в какой-то мере!
Редактировать:
Причина, по которой ему необходимо отслеживать количество элементов, заключается в том, чтобы он мог перебирать их, когда вы вызываете delete в массиве и убедитесь, что деструкторы вызываются для каждого из объектов. Если он не знает, сколько их, то не сможет этого сделать.
VC++ - плохой компилятор. Новый массив размещения по умолчанию, известный как new(void*) type[n], выполняет построение на месте объектов n из type. Предоставленный указатель должен быть правильно выровнен, чтобы соответствовать alignof(type) (примечание: sizeof(type) является кратным alignof(type) из-за заполнения). Поскольку обычно вам придется переносить длину массива, нет фактического требования хранить его внутри массива, потому что вы все равно собираетесь уничтожить его с помощью цикла for (без операторов удаления размещения).
@ bit2shift Стандарт C++ явно указывает, что new[] заполняет выделенную память, хотя допускает заполнение 0 байтов. (См. Раздел «expr.new», особенно примеры (14.3) и (14.4) и объяснение под ними.) Таким образом, в этом отношении он действительно соответствует стандарту. [Если у вас нет копии окончательной версии стандарта C++ 14, см. Стр. 133 здесь.]
@JustinTime Это дефект, на который никто не обращает внимания.
@ bit2shift Если это разрешено стандартом, почему это дефект, если какой-либо компилятор действует соответствующим образом? Или вы говорите, что это дефект самого стандарта?
@JustinTime Это дефект в самом стандарте, позволяющий накладывать накладные расходы на void* operator new[](std::size_t count, void* ptr);, когда известно, что этот оператор является "no-op" (без распределения), и также ясно известно, что указатель, возвращаемый этим оператором или его скалярным родственником не может быть передан ни в delete, ни в delete[], требуя, чтобы программист вручную уничтожил каждый элемент.
@ bit2shift Ах, в этом есть смысл. Похоже, что накладные расходы должны быть применимы к любой версии operator new[], предполагая, что это, вероятно, обходной путь для компиляторов или целевых систем, которые требуют дополнительных накладных расходов при создании массивов и, вероятно, предназначены для согласованности между различными версиями new[]. Вероятно, это потребует минимизации накладных расходов или их отсутствия для перегрузок размещения, тем не менее, чтобы предотвратить как можно больше неприятных сюрпризов.
Разрушение должно происходить в порядке, обратном построению. Сделайте так, чтобы цикл переместился с NUMELEMENTS на 0. В этом случае это не имеет значения, но это имеет значение в общем случае (более поздние объекты могут зависеть от предыдущих), и кто-то, копирующий код, может не знать, что
Он не компилируется. В частности, нарушается назначение pA[i] = new (pA + i) A();.
pA[i] = следует опустить для его компиляции, или вы можете заменить весь цикл for на std::uninitialized_default_construct_n (это дает преимущество обработки исключений во время построения). В любом случае у вас по-прежнему будет неопределенное поведение, если вы не запустите указатель, используемый для доступа к созданным объектам. Однако я не уверен, достаточно ли pA = std::launder(pA) или вам нужно будет отмывать указатель для каждого отдельного объекта, поскольку вы не строили массив, вы создали отдельные объекты, которые просто находятся рядом в памяти.
Что касается обработки набора смежных объектов, индивидуально созданных в непрерывной памяти, как объекта массива, см. Проблема ядра C++ 2182. К сожалению, на самом деле это, похоже, еще не решено, а это означает, что не может быть никакого способа создать объект массива на месте, который гарантированно будет работать по стандарту без какого-либо неопределенного поведения. Эта проблема также анализируется в это видео.
Подобно тому, как вы использовали бы один элемент для расчета размера для одного нового размещения, используйте массив этих элементов для расчета размера, необходимого для массива.
Если вам требуется размер для других вычислений, где количество элементов может быть неизвестно, вы можете использовать sizeof (A [1]) и умножить его на необходимое количество элементов.
например
char *pBuffer = new char[ sizeof(A[NUMELEMENTS]) ];
A *pA = (A*)pBuffer;
for(int i = 0; i < NUMELEMENTS; ++i)
{
pA[i] = new (pA + i) A();
}
Дело в том, что MSVC явно требует дополнительного места сверх значения sizeof(A[NUMELEMENTS]) в случае new []. sizeof(A[N]) будет просто N * sizeof(N) в целом и не будет отражать это дополнительное необходимое пространство.
Спасибо за ответы. Использование нового размещения для каждого элемента в массиве было решением, которое я в конечном итоге использовал, когда столкнулся с этим (извините, я должен был упомянуть об этом в вопросе). Я просто почувствовал, что, должно быть, я чего-то упускал из-за того, что делал это с размещением new []. Как бы то ни было, кажется, что размещение new [] по существу непригодно для использования благодаря стандарту, позволяющему компилятору добавлять в массив дополнительные неуказанные накладные расходы. Я не понимаю, как его можно безопасно и портативно использовать.
Я даже не совсем понимаю, зачем ему нужны дополнительные данные, поскольку вы все равно не вызовете delete [] в массиве, поэтому я не совсем понимаю, почему ему нужно знать, сколько элементов в нем.
@Джеймс
I'm not even really clear why it needs the additional data, as you wouldn't call delete[] on the array anyway, so I don't entirely see why it needs to know how many items are in it.
Поразмыслив, я согласен с вами. Нет причин, по которым при размещении new должно храниться количество элементов, потому что нет места размещения delete. Поскольку нет места удаления, нет причин размещать new для хранения количества элементов.
Я также протестировал это с помощью gcc на своем Mac, используя класс с деструктором. В моей системе новым размещением было изменение указателя нет. Это заставляет меня задаться вопросом, является ли это проблемой VC++ и может ли это нарушать стандарт (насколько я могу судить, в стандарте это конкретно не рассматривается).
Я тестировал как clang 3.7.0, так и GCC 5.3.0, оба с -std=c++14 и -pedantic на Колиру. Ни один из них не показал наличия накладных расходов, особенно с классами с нетривиальными деструкторами. Итак, я думаю, это еще один пример того, насколько плохой компилятор Visual C++.
Я очень сомневаюсь, что @ Nik-Lz, учитывая, что они по-прежнему выставляют напоказ отвратительный nothrownew.obj как способ заставить new вести себя как new(std::nothrow).
@Derek
В разделе 5.3.4, раздел 12 говорится о накладных расходах на выделение массива, и, если я не неправильно его читаю, мне кажется, что компилятор может также добавить его при размещении new:
This overhead may be applied in all array new-expressions, including those referencing the library function operator new[](std::size_t, void*) and other placement allocation functions. The amount of overhead may vary from one invocation of new to another.
Тем не менее, я думаю, что VC был единственным компилятором, который доставил мне проблемы с этим, помимо GCC, Codewarrior и ProDG. Однако мне придется проверить еще раз, чтобы убедиться.
Я был бы шокирован, если бы VC был единственным компилятором, добавляющим дополнительное пространство, я бы подумал, что все компиляторы сохранят количество вызываемых деструкторов. Нет другого логичного места для этого.
@MooingDuck нет оператора удаления размещения, который использовал бы вышеупомянутую «длину массива». Фактически, вам нужно вручную вызвать деструкторы для массива построен с new(pointer) type[length]. VC - плохой компилятор, это должен знать каждый.
Размещение new само по себе переносимо, но предположения, которые вы делаете о том, что оно делает с указанным блоком памяти, непереносимы. Как было сказано ранее, если бы вы были компилятором и получили кусок памяти, как бы вы узнали, как выделить массив и правильно уничтожить каждый элемент, если бы все, что у вас было, было указателем? (См. Интерфейс оператора delete [].)
Редактировать:
И на самом деле есть удаление размещения, только оно вызывается только тогда, когда конструктор генерирует исключение при выделении массива с размещением new [].
Вопрос о том, действительно ли new [] должен каким-то образом отслеживать количество элементов, остается на усмотрение стандарта, который оставляет это на усмотрение компилятора. К сожалению, в этом случае.
Как он может быть «портативным», если он перезаписывает произвольно большой объем памяти? Вы никогда не сможете безопасно назвать это, потому что никогда не знаете, сколько он будет писать.
Выражение delete[] должно знать, но его можно использовать только в результате выражения new[] с выделением кучи (либо бросанием, либо nothrow). Для размещения new[] это действительно не нужно. Единственное объяснение, которое у меня есть для поведения VC, заключается в том, что куча new[] каким-то образом реализована с точки зрения размещения new[] и не хранит количество элементов.
Ах, проклятия. Я задурачил твой вопрос :( Новое размещение массива требует неопределенных накладных расходов в буфере?