Можно ли переносить размещение new для массивов?

Можно ли действительно использовать размещение 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 [], будет таким же, как адрес, который я передал.

Ответов (7)

Решение

Лично я бы предпочел не использовать размещение 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 в массиве и убедитесь, что деструкторы вызываются для каждого из объектов. Если он не знает, сколько их, то не сможет этого сделать.

Я думаю, что gcc делает то же самое, что и MSVC, но, конечно, это не делает его «переносимым».

Я думаю, вы можете обойти проблему, если NUMELEMENTS действительно является постоянной времени компиляции, например:

typedef A Arr[NUMELEMENTS];

A* p = new (buffer) Arr;

Это должно использовать скалярное размещение new.

Спасибо за ответы. Использование нового размещения для каждого элемента в массиве было решением, которое я в конечном итоге использовал, когда столкнулся с этим (извините, я должен был упомянуть об этом в вопросе). Я просто почувствовал, что, должно быть, я чего-то упускал из-за того, что делал это с размещением new []. Как бы то ни было, кажется, что размещение new [] по существу непригодно для использования благодаря стандарту, позволяющему компилятору добавлять дополнительные неуказанные накладные расходы в массив. Я не понимаю, как его можно безопасно и портативно использовать.

Я даже не совсем понимаю, зачем ему нужны дополнительные данные, поскольку вы все равно не вызываете delete [] в массиве, поэтому я не совсем понимаю, почему ему нужно знать, сколько элементов в нем.

Подобно тому, как вы использовали бы один элемент для расчета размера для одного нового размещения, используйте массив этих элементов для расчета размера, необходимого для массива.

Если вам требуется размер для других вычислений, где количество элементов может быть неизвестно, вы можете использовать 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();
}

@Джеймс

Я даже не совсем понимаю, зачем ему нужны дополнительные данные, поскольку вы все равно не вызываете delete [] в массиве, поэтому я не совсем понимаю, почему ему нужно знать, сколько элементов в нем.

Поразмыслив над этим, я согласен с вами. Нет причин, по которым при размещении new должно храниться количество элементов, потому что нет места размещения delete. Поскольку нет места удаления, нет причин размещать new для хранения количества элементов.

Я также протестировал это с помощью gcc на своем Mac, используя класс с деструктором. В моей системе размещение new не изменяло указатель. Это заставляет меня задаться вопросом, является ли это проблемой VC++ и может ли это нарушать стандарт (насколько я могу судить, в стандарте это конкретно не рассматривается).

@Derek

В разделе 5.3.4, раздел 12 говорится о накладных расходах на выделение массива, и, если я не неправильно его читаю, мне кажется, что компилятор может также добавить его при размещении new:

Эти накладные расходы могут применяться во всех новых выражениях массива, включая те, которые ссылаются на оператор библиотечной функции new [] (std::size_t, void *) и другие функции размещения. Сумма накладных расходов может варьироваться от одного вызова нового к другому.

Тем не менее, я думаю, что VC был единственным компилятором, который доставил мне проблемы с этим, помимо GCC, Codewarrior и ProDG. Однако мне придется проверить еще раз, чтобы убедиться.

Размещение new само по себе переносимо, но предположения, которые вы делаете о том, что оно делает с указанным блоком памяти, непереносимы. Как было сказано ранее, если бы вы были компилятором и получили кусок памяти, как бы вы узнали, как выделить массив и правильно уничтожить каждый элемент, если бы все, что у вас было, было указателем? (См. Интерфейс оператора delete [].)

Редактировать:

И на самом деле есть удаление размещения, только оно вызывается только тогда, когда конструктор генерирует исключение при выделении массива с размещением new [].

Вопрос о том, действительно ли new [] должен каким-то образом отслеживать количество элементов, остается на усмотрение стандарта, а это оставляет это на усмотрение компилятора. К сожалению, в этом случае.