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

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

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

8 ответов

Решение

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

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

@Derek

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

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

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

@Джеймс

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

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

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

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

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

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

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

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

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

С++17 (черновик N4659) говорит в [expr.new], параграф 15:

[O]verhead может применяться во всех новых-выражениях массива , включая те, которые ссылаются на библиотечную функцию и другие функции распределения мест размещения. Сумма накладных расходов может варьироваться от одного вызова к другому.

Таким образом, кажется, что это невозможно использоватьбезопасное размещение в С++17 (и более ранних версиях), и мне неясно, почему он вообще существует.

В C++20 (черновик N4861) это было изменено на

[O]verhead может применяться во всех выражениях массива new-expressions , включая те, которые ссылаются на функцию распределения мест размещения, за исключением случаев, когда они ссылаются на библиотечную функцию.. Сумма накладных расходов может варьироваться от одного вызова к другому.

Поэтому, если вы уверены, что используете C++20, вы можете безопасно использовать его, но только эту одну форму размещения и только (она появляется), если вы не переопределяете стандартное определение.

Даже текст C++20 кажется нелепым, потому что единственное назначение дополнительного пространства — хранить метаданные размера массива, но нет никакого способа получить к нему доступ при использовании любой пользовательской формы размещения. Это частный формат, который умеет только читать, а с пользовательским распределением вы не можете использовать, так что в лучшем случае это просто пустая трата места.

На самом деле, насколько я могу судить, вообще не существует безопасного способа использования пользовательских форм. Невозможно правильно вызвать деструкторы, потому что необходимая информация не передается в . Даже если вы знаете, что объекты тривиально разрушаемы,выражение может возвращать указатель на какое-то произвольное место в середине блока памяти, которое вы вернули (пропуская бессмысленные метаданные), поэтому вы не можете обернуть библиотеку распределения, которая только предоставляетиэквиваленты: ему также нужен способ поиска блока по указателю на его середину, который, даже если он существует, вероятно, будет намного медленнее.

Я не понимаю, как они (или просто Страуструп?) так плохо все испортили. Очевидно правильный способ сделать это — передать количество элементов массива и размер каждого элемента вкак два аргумента, и пусть каждый распределитель выбирает, как его хранить. Возможно, я что-то упускаю.

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

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

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

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

typedef A Arr[NUMELEMENTS];

A* p = new (buffer) Arr;

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

Другие вопросы по тегам