Можно ли использовать новое размещение для массивов переносимым способом?
-
08-06-2019 - |
Вопрос
Возможно ли на самом деле использовать placement 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
большой, последний элемент в массиве записывается в нераспределенную кучу.
Итак, вопрос в том, можете ли вы выяснить, сколько дополнительных накладных расходов требуется вашей реализации для безопасного использования placement new[]?В идеале мне нужна техника, переносимая между различными компиляторами.Обратите внимание, что, по крайней мере, в случае VC, накладные расходы, похоже, различаются для разных классов.Например, если я удалю виртуальный деструктор в примере, адрес, возвращаемый из 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 в массиве, и убедиться, что деструкторы вызываются для каждого из объектов.Если бы он не знал, сколько их там, он не смог бы этого сделать.
Другие советы
@Дерек
5.3.4, раздел 12 рассказывает о накладных расходах на распределение массива, и, если я не ошибаюсь, мне кажется, это наводит на мысль, что компилятору также допустимо добавлять его при размещении new:
Эти служебные данные могут применяться во всех новых выражениях массива, включая те, которые ссылаются на библиотечную функцию operator new[](std::size_t, void*) и другие функции распределения размещения.Объем накладных расходов может варьироваться от одного вызова new к другому.
Тем не менее, я думаю, что VC был единственным компилятором, который доставлял мне проблемы с этим, за исключением GCC, Codewarrior и ProDG.Однако мне придется проверить еще раз, чтобы быть уверенным.
Спасибо за ответы.Использование placement new для каждого элемента в массиве было решением, которое я в конечном итоге использовал, когда столкнулся с этим (извините, следовало упомянуть об этом в вопросе).Я просто почувствовал, что, должно быть, мне чего-то не хватало в том, чтобы делать это с placement new [].Как бы то ни было, кажется, что размещение new[] по существу непригодно для использования благодаря стандарту, позволяющему компилятору добавлять дополнительные неуказанные служебные данные к массиву.Я не понимаю, как вы вообще могли бы использовать его безопасно и переносимо.
Я даже не совсем понимаю, зачем ему нужны дополнительные данные, поскольку вы все равно не стали бы вызывать delete[] в массиве, поэтому я не совсем понимаю, зачем ему знать, сколько в нем элементов.
@Джеймс
Я даже не совсем понимаю, зачем ему нужны дополнительные данные, поскольку вы все равно не стали бы вызывать delete[] в массиве, поэтому я не совсем понимаю, зачем ему знать, сколько в нем элементов.
Немного поразмыслив над этим, я соглашаюсь с вами.Нет причин, по которым placement new должен сохранять количество элементов, потому что нет placement delete.Поскольку удаление размещения отсутствует, нет причин для размещения new для сохранения количества элементов.
Я также протестировал это с помощью gcc на моем Mac, используя класс с деструктором.В моей системе новое размещение было нет изменение указателя.Это заставляет меня задаться вопросом, является ли это проблемой VC ++, и может ли это нарушать стандарт (стандарт конкретно не рассматривает это, насколько я могу найти).
Placement new сам по себе является переносимым, но предположения, которые вы делаете о том, что он делает с указанным блоком памяти, не являются переносимыми.Как было сказано ранее, если бы вы были компилятором и вам был предоставлен кусок памяти, откуда бы вы узнали, как выделить массив и правильно уничтожить каждый элемент, если бы все, что у вас было, - это указатель?(Смотрите интерфейс оператора delete[].)
Редактировать:
И на самом деле есть удаление размещения, только оно вызывается только тогда, когда конструктор генерирует исключение при выделении массива с размещением new[].
Действительно ли new[] нужно каким-то образом отслеживать количество элементов, зависит от стандарта, который оставляет это на усмотрение компилятора.К сожалению, в данном случае.
Я думаю, что gcc делает то же самое, что и MSVC, но, конечно, это не делает его "переносимым".
Я думаю, вы можете обойти проблему, когда NUMELEMENTS действительно является константой времени компиляции, вот так:
typedef A Arr[NUMELEMENTS];
A* p = new (buffer) Arr;
При этом следует использовать скалярное размещение new.
Аналогично тому, как вы использовали бы один элемент для вычисления размера для одного размещения-new, используйте массив этих элементов для вычисления размера, необходимого для массива.
Если вам требуется размер для других вычислений, где количество элементов может быть неизвестно, вы можете использовать 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();
}