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

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

Похоже, что указатель, который вы возвращаете из нового [], не всегда совпадает с адресом, который вы передаете (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, накладные расходы, по-видимому, различаются для разных классов. Например, если я удалю виртуальный деструктор в примере, адрес, возвращаемый с нового [], совпадает с адресом, который я передаю.

Ответ 1

Лично я бы пошел с возможностью не использовать размещение 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, так как вы могли бы получить утечки;)

Примечание. Я не скомпилировал это, но я думаю, что он должен работать (я нахожусь на машине, на которой не установлен компилятор С++). Это все еще указывает на точку:) Надеюсь, это поможет в некотором роде!


Edit:

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

Ответ 2

@Derek

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

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

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

Ответ 3

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

Edit:

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

Нужно ли новому [] на самом деле отслеживать количество элементов, что-то, что осталось до стандарта, что оставляет его компилятору. К сожалению, в этом случае.

Ответ 4

@James

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

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

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

Ответ 5

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

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

typedef A Arr[NUMELEMENTS];

A* p = new (buffer) Arr;

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

Ответ 6

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

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

Ответ 7

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

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