Массив размещения-новый требует неопределенных накладных расходов в буфере?

5.3.4 [expr.new] проекта C++11 февраля приводит пример:

new(2,f) T[5] приводит к вызову operator new[](sizeof(T)*5+y,2,f),

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

Теперь возьмите следующий пример кода:

void* buffer = malloc(sizeof(std::string) * 10);
std::string* p = ::new (buffer) std::string[10];

Согласно приведенной цитате, вторая строка new (buffer) std::string[10] внутренне позвонит operator new[](sizeof(std::string) * 10 + y, buffer) (перед построением личности std::string объекты). Проблема в том, что если y > 0предварительно выделенный буфер будет слишком маленьким!

Так как же узнать, сколько памяти нужно предварительно выделить при использовании массива размещения новых?

void* buffer = malloc(sizeof(std::string) * 10 + how_much_additional_space);
std::string* p = ::new (buffer) std::string[10];

Или стандарт где-то гарантирует, что y == 0 в этом случае? Опять цитата говорит:

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

7 ответов

Решение

Не использовать operator new[](std::size_t, void* p) если вы не знаете априори ответ на этот вопрос. Ответ - деталь реализации и может меняться в зависимости от компилятора / платформы. Хотя это обычно стабильно для любой данной платформы. Например, это то, что указано в Itanium ABI.

Если вы не знаете ответ на этот вопрос, напишите свой собственный новый массив размещения, который может проверить это во время выполнения:

inline
void*
operator new[](std::size_t n, void* p, std::size_t limit)
{
    if (n <= limit)
        std::cout << "life is good\n";
    else
        throw std::bad_alloc();
    return p;
}

int main()
{
    alignas(std::string) char buffer[100];
    std::string* p = new(buffer, sizeof(buffer)) std::string[3];
}

Изменяя размер массива и проверяя n в приведенном выше примере вы можете сделать вывод y для вашей платформы. Для моей платформы y это 1 слово. Размер слова зависит от того, собираюсь ли я для 32-битной или 64-битной архитектуры.

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

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

Я повторю этот вопрос здесь, насколько я понимаю, в надежде, что более короткая версия может помочь другим понять, о чем спрашивают. Вопрос в том:

Всегда ли правильна следующая конструкция? Является arr == addr в конце?

void * addr = std::malloc(N * sizeof(T));
T * arr = ::new (addr) T[N];                // #1

Из стандарта мы знаем, что #1 вызывает вызов ::operator new[](???, addr), где ??? неуказанное число не меньше чем N * sizeof(T) и мы также знаем, что этот вызов только возвращает addr и не имеет других эффектов. Мы также знаем, что arr смещен от addr соответственно. То, что мы не знаем, является ли память, на которую указывает addr достаточно велик, или как бы мы знали, сколько памяти выделить.


Вы, кажется, путаете несколько вещей:

  1. Ваш пример звонков operator new[]() не operator new(),

  2. Функции распределения ничего не создают. Они выделяют.

Что происходит, что выражение T * p = new T[10]; причины:

  1. вызов operator new[]() с аргументом размера 10 * sizeof(T) + x,

  2. десять вызовов к конструктору по умолчанию T эффективно ::new (p + i) T(),

Единственная особенность в том, что выражение " новый массив" запрашивает больше памяти, чем то, что используется самими данными массива. Вы не видите ничего из этого и не можете использовать эту информацию иначе, как путем молчаливого принятия.


Если вам интересно, сколько памяти фактически было выделено, вы можете просто заменить функции выделения массива. operator new[] а также operator delete[] и заставьте это распечатать фактический размер.


Обновление: как случайная часть информации, вы должны заметить, что глобальные новые функции размещения должны быть неактивными. То есть, когда вы создаете объект или массив на месте следующим образом:

T * p = ::new (buf1) T;
T * arr = ::new (buf10) T[10];

Тогда соответствующие звонки ::operator new(std::size_t, void*) а также ::operator new[](std::size_t, void*) ничего не делай, только верни свой второй аргумент. Тем не менее, вы не знаете, что buf10 должен указывать на: он должен указывать на 10 * sizeof(T) + y байтов памяти, но вы не можете знать y,

Звонок любой версии operator new[] () не будет работать слишком хорошо с областью памяти фиксированного размера. По сути, предполагается, что он делегирует некоторую реальную функцию выделения памяти, а не просто возвращает указатель на выделенную память. Если у вас уже есть арена памяти, где вы хотите построить массив объектов, вы хотите использовать std::uninitialized_fill() или же std::uninitialized_copy() конструировать объекты (или какую-то другую форму индивидуального конструирования объектов).

Вы можете возразить, что это означает, что вам придется уничтожать объекты на арене памяти также вручную. Тем не менее, призывая delete[] array по указателю, возвращенному из места размещения new не будет работать: он будет использовать версию без размещения operator delete[] ()! То есть при использовании размещения new вам нужно вручную уничтожить объект (ы) и освободить память.

Как упомянуто Kerrek SB в комментариях, этот дефект был впервые зарегистрирован в 2004 году, и он был устранен в 2012 году как:

CWG решила, что EWG является подходящим местом для решения этой проблемы.

Затем о дефекте было сообщено EWG в 2013 году, но оно было закрыто как NAD (предположительно означает "не дефект") с комментарием:

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

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


Следствие, не упомянутое в другом месте в потоке, состоит в том, что этот код вызывает неопределенное поведение для всех T:

T *ptr = new T[N];
::operator delete[](ptr);

Даже если мы соблюдаем правила жизни (т.е. T либо имеет тривиальное разрушение, либо программа не зависит от побочных эффектов деструктора), проблема в том, что ptr был откорректирован для этого неуказанного куки, поэтому это неправильное значение для передачи operator delete[],

Обратите внимание, что C++20 меняет этот ответ.

В C++17 (и ранее) [expr.new] / 11 ясно сказано, что эта функция может получить смещение, определяемое реализацией, к ее размеру:

Когда новое-выражение вызывает функцию выделения и это выделение не было расширено, новое-выражение передает запрошенный объем пространства в функцию выделения в качестве первого аргумента типа std ::size_t. Этот аргумент должен быть не меньше размера создаваемого объекта; он может быть больше размера создаваемого объекта, только если объект является массивом.

Это позволяет, но не требует , чтобы размер, присвоенный функции распределения массива, мог быть увеличен с sizeof(T) * size.

C++20 явно запрещает это. Из [expr.new] / 15:

Когда новое-выражение вызывает функцию выделения и это выделение не было расширено, новое-выражение передает запрошенный объем пространства в функцию выделения в качестве первого аргумента типа std ::size_t. Этот аргумент должен быть не меньше размера создаваемого объекта; он может быть больше, чем размер создаваемого объекта, только если объект является массивом, а функция распределения не является формой без выделения ([new.delete.placement]).

Акцент добавлен. Даже цитируемое вами ненормативное примечание было изменено:

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

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

Это дефект в стандарте. По слухам, они не смогли найти добровольца, чтобы написать исключение (Сообщение № 1165).

Несменный массив размещения new не может быть использован с delete[] выражений, поэтому вам нужно перебрать массив и вызвать каждый деструктор.

Накладные расходы нацелены на определенные пользователем функции размещения массива, которые выделяют память точно так же, как обычные T* tp = new T[length], Те совместимы с delete[] следовательно, накладные расходы, которые несут длину массива.

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

Новое выражение пытается создать объект typeid (8.1) или newtypeid, к которому он применяется. Тип этого объекта - выделенный тип. Этот тип должен быть полным типом объекта, но не абстрактный тип класса или его массив (1.8, 3.9, 10.4). [Примечание: поскольку ссылки не являются объектами, ссылки не могут быть созданы новыми выражениями. ] [Примечание: typeid может быть cvqualified типом, и в этом случае объект, созданный new expression, имеет cvqualified тип. ]

new-expression: 
    ::(opt) new new-placement(opt) new-type-id new-initializer(opt)
    ::(opt) new new-placement(opt) ( type-id ) new-initializer(opt)

new-placement: ( expression-list )

newtypeid:
    type-specifier-seq new-declarator(opt)

new-declarator:
    ptr-operator new-declarator(opt)
    direct-new-declarator

direct-new-declarator:
    [ expression ]
    direct-new-declarator [ constant-expression ]

new-initializer: ( expression-list(opt) )

Мне кажется, что array placement new просто проистекает из компактности определения (все возможные варианты использования как одна схема), и, похоже, нет веских причин для его запрета.

Это оставляет нас в ситуации, когда у нас есть бесполезный оператор, которому требуется выделенная память, прежде чем станет известно, сколько его потребуется. Единственные решения, которые я вижу, это либо перераспределить память, и надеяться, что компилятор не захочет больше, чем поставлено, либо перераспределить память в переопределенной array placement new функция / метод (который скорее побеждает цель использования array placement new на первом месте).


Чтобы ответить на вопрос, указанный Kerrek SB: Ваш пример:

void * addr = std::malloc(N * sizeof(T));
T * arr = ::new (addr) T[N];                // #1

не всегда правильно. В большинстве реализаций arr!=addr (и тому есть веские причины), поэтому ваш код недействителен, и ваш буфер будет переполнен.

Об этих "веских причинах" - обратите внимание, что стандартные создатели освобождают вас от некоторого домашнего хозяйства при использовании array new оператор и array placement new ничем не отличается в этом отношении. Обратите внимание, что вам не нужно сообщать delete[] о длине массива, поэтому эта информация должна храниться в самом массиве. Куда? Именно в этой дополнительной памяти. Без этого delete[]Для 'ing' потребуется разделить длину массива (как в stl, используя циклы и не размещение) new)

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