Можно ли использовать новые массивы для переноса? - PullRequest
36 голосов
/ 19 августа 2008

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

Похоже, что указатель, который вы получаете от 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 ]

27 голосов
/ 19 августа 2008

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

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 ++). Это все еще указывает на смысл :) Надеюсь, что это поможет каким-то образом!


Edit:

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

4 голосов
/ 19 августа 2008

@ Derek

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

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

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

2 голосов
/ 20 августа 2008

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

Edit:

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

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

2 голосов
/ 19 августа 2008

@ Джеймс

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

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

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

2 голосов
/ 19 августа 2008

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

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

1 голос
/ 19 августа 2008

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

Если вам требуется размер для других вычислений, когда число элементов может быть неизвестно, вы можете использовать sizeof (A [1]) и умножить на необходимое количество элементов.

* 1005 например *

char *pBuffer = new char[ sizeof(A[NUMELEMENTS]) ];
A *pA = (A*)pBuffer;

for(int i = 0; i < NUMELEMENTS; ++i)
{
    pA[i] = new (pA + i) A();
}
1 голос
/ 19 августа 2008

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

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

typedef A Arr[NUMELEMENTS];</p> <p>A* p = new (buffer) Arr;

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

...