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

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

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

18.08.2008 21:33:35
Mooing Duck 7.05.2015 18:51:24
Хм ... если накладные расходы исчезают при удалении виртуального деструктора, это может указывать на то, что накладные расходы, скорее всего, связаны либо с классом vtable, либо с реализацией RTTI в VStudio.
Justin Time - Reinstate Monica 25.05.2016 18:33:02
Или, по крайней мере, часть накладных расходов. Также возможно, что накладные расходы используются только в том случае, если в классе есть нетривиальный деструктор.
Justin Time - Reinstate Monica 25.05.2016 18:50:30
7 ОТВЕТОВ
РЕШЕНИЕ

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

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

26
13.03.2015 06:52:31
VC ++ плохой компилятор. По умолчанию размещение нового массива aka new(void*) type[n]выполняет на месте построение nобъектов type. Указанный указатель должен быть правильно выровнен для соответствия alignof(type)(примечание: sizeof(type)кратно из- alignof(type)за заполнения). Поскольку вам обычно приходится иметь дело с длиной массива, нет фактического требования хранить его внутри массива, потому что вы все равно собираетесь уничтожить его с помощью цикла for (без операторов удаления размещения).
bit2shift 11.03.2016 21:45:03
@ bit2shift Стандарт C ++ явно заявляет, что дополняет new[]выделенную память, хотя он допускает заполнение 0 байтов. (См. Раздел «expr.new», в частности примеры (14.3) и (14.4) и пояснения под ними.) Таким образом, в этом отношении он фактически соответствует стандартам. [Если у вас нет копии окончательной версии стандарта C ++ 14, см. Стр. 133. здесь .]
Justin Time - Reinstate Monica 25.05.2016 18:44:23
@JustinTime Это дефект, на который никто, похоже, не смотрит.
bit2shift 26.05.2016 21:41:19
@JustinTime В самом стандарте есть дефект, позволяющий применять накладные расходы, void* operator new[](std::size_t count, void* ptr);когда этот оператор, как известно, является «безоперационным» (без выделения), и также ясно известно, что указатель, возвращаемый этим оператором или его скалярным родственным элементом, не может быть переданным либо, deleteлибо delete[], требуя, чтобы программист вручную уничтожил каждый элемент.
bit2shift 29.05.2016 02:29:38
Относительно обработки набора смежных объектов, индивидуально созданных в непрерывной памяти, как объекта массива, см. C ++ Core Issue 2182 . К сожалению, это, похоже, еще не решено, а это значит, что не может быть никакого способа на месте создать объект массива, который гарантированно будет работать по стандарту без какого-либо неопределенного поведения. Эта проблема также анализируется в этом видео .
Matthijs 3.10.2019 23:00:52

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

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

typedef A Arr[NUMELEMENTS];

A* p = new (buffer) Arr;

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

0
18.08.2008 21:45:14
Это не так. operator new()vs operator new[]()зависит от того, задействован ли тип массива, а не от того, были ли они []в исходном коде.
Ben Voigt 27.06.2014 15:08:27

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

Если вам требуется размер для других вычислений, где число элементов может быть неизвестно, вы можете использовать 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();
}
1
18.08.2008 23:26:37
Дело в том, что MSVC, видимо, требует дополнительного пространства сверх значения sizeof(A[NUMELEMENTS])в случае new []. sizeof(A[N])просто будет N * sizeof(N)в общем и не будет отражать это дополнительное необходимое пространство.
BeeOnRope 9.10.2017 07:53:29

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

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

2
19.08.2008 00:03:57

@Джеймс

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

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

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

2
19.08.2008 02:14:14
Я тестировал как clang 3.7.0, так и GCC 5.3.0, как на Coliru , так -std=c++14и -pedanticна нем . Ни один из них не показал существования накладных расходов, особенно с классами с нетривиальными деструкторами. Итак, я думаю, это еще один пример того, как Visual C ++ такой плохой компилятор.
bit2shift 11.03.2016 21:17:08
@ bit2shift это было за 8 лет до вашего комментария (а ваш комментарий за 2 года до моего), поэтому я хотел бы верить, что все исправлено.
Nikos 4.09.2018 19:20:08
Я очень сомневаюсь в этом, @ Nik-Lz, учитывая, что они все еще выставляют напоказ отвратительный nothrownew.objспособ заставить newсебя вести себя как new(std::nothrow).
bit2shift 5.09.2018 04:05:46

@Derek

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

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

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

4
19.08.2008 10:16:44
Я был бы шокирован, если бы VC был единственным компилятором, добавляющим дополнительное пространство, я бы подумал, что все компиляторы будут хранить количество деструкторов для вызова там. Там нет другого логического места, чтобы положить это.
Mooing Duck 7.05.2015 19:02:47
@MooingDuck нет оператора удаления размещения, чтобы использовать вышеупомянутую «длину массива». На самом деле, вы должны вручную вызывать деструкторы для массива, созданного с помощью new(pointer) type[length]. VC - плохой компилятор, все должны это знать.
bit2shift 11.03.2016 21:23:00

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

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

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

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

2
3.07.2012 14:51:43
Как он может быть «переносимым», если он перезаписывает произвольно большой объем памяти? Вы никогда не могли бы безопасно назвать это, так как вы никогда не знаете, сколько он собирается написать.
BeeOnRope 19.10.2017 22:18:18
В delete[]потребности выражения , чтобы знать, но он может быть использован только на результате new[]выражения с кучей распределения (либо бросать или nothrow). Для размещения new[]это действительно не нужно. Единственное объяснение, которое я имею для поведения VC, состоит в том, что куча new[]каким-то образом реализована с точки зрения размещения new[]и не хранит количество элементов.
Arne Vogel 9.01.2018 18:40:24