Подтвердить что ты не робот

Очевидное противоречие между книгой Страуступа и стандартом С++

Я пытаюсь понять следующий абзац из Stroustrup "Язык программирования С++" на стр. 282 (акцент мой):

Чтобы освободить место, выделенное новым, удалить и удалить [], должно быть возможно для определения размера выделенного объекта. Это означает, что объект, выделенный с использованием стандартной реализации нового, будет занимать немного больше места, чем статический объект. Как минимум, пространство необходимо сохранить размер объектов. Обычно два или более слова на распределения используются для управления свободными магазинами. Большинство современных машин используйте 8-байтные слова. Эти накладные расходы незначительны при распределении многие объекты или крупные объекты, но это может иметь значение, если мы выделяем лоты небольших объектов (например, ints или Points) в свободном хранилище.

Обратите внимание, что автор не различает, является ли объект массивом, или нет, в предложении, выделенном выше.

Но в соответствии с параграфом §5.3.4/11 в С++ 14 мы имеем (мой акцент):

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

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

4b9b3361

Ответ 1

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

Когда выделяется массив объектов с нетривиальным деструктором, реализация должна каким-то образом узнать, сколько раз вызывать деструктор при вызове delete[]. Реализациям разрешено выделять дополнительное пространство вместе с массивом для хранения этой дополнительной информации, но не каждая реализация работает таким образом.

Ответ 2

Если вы вызываете new для типа T, перегруженный operator new, который может быть вызван, будет передан точно sizeof(T).

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

Это примерно то, что большинство стандартных реализаций newmalloc do).

Поэтому, когда вам нужно sizeof(T) байтов для хранения T, количество байтов, потребляемых new/malloc, больше, чем sizeof(T). Об этом говорит Страуструп: каждое динамическое распределение имеет фактические накладные расходы, и что накладные расходы могут быть существенными, если вы делаете много небольших распределений.


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

Здесь учетная информация не хранится рядом с данными, или мы делаем учетную информацию неявной в состоянии кода (ограниченные распределители).


Теперь, в случае массивов, компилятор С++ может вызывать operator new[] с объемом запрашиваемой памяти, превышающим sizeof(T)*n, когда выделяется T[n]. Это делается с помощью кода new (not operator new), сгенерированного компилятором, когда он запрашивает вашу перегрузку для памяти.

Это традиционно выполняется по типам с нетривиальными деструкторами, так что среда выполнения С++ может, когда вызывается delete[], перебирать каждый элемент и называть .~T() на них. Он вытаскивает подобный трюк, в котором он хранит n в памяти перед массивом, который он использует, затем выполняет арифметику указателя, чтобы извлечь его во время удаления.

Это не требуется стандартом, но это обычная техника (clang и gcc оба делают это хотя бы на некоторых платформах, и я считаю, что MSVC тоже). Требуется некоторый метод вычисления размера массива; это только один из них.

Для чего-то без деструктора (например, char) или тривиального (например, struct foo{ ~foo()=default; }, n не требуется время выполнения, поэтому ему не нужно его хранить, поэтому он может сказать: naw, я не буду хранить его ".

Вот живой пример.

struct foo {
  static void* operator new[](std::size_t sz) {
    std::cout << sz << '/' << sizeof(foo) << '=' << sz/sizeof(foo) << "+ R(" << sz%sizeof(foo) << ")" << '\n';
    return malloc(sz);
  }
  static void operator delete[](void* ptr) {
    free(ptr);
  }
  virtual ~foo() {}
};

foo* test(std::size_t n) {
  std::cout << n << '\n';
  return new foo[n];
}

int main(int argc, char**argv) {
  foo* f = test( argc+10 );
  std::cout << *std::prev(reinterpret_cast<std::size_t*>(f)) << '\n';
}

Если выполняется с аргументами 0, он выводит 11, 96/8 = 12 R(0) и 11.

Во-первых, количество выделенных элементов, второе - сколько памяти выделено (что добавляет до 11 элементов, плюс 8 байт - sizeof(size_t), я подозреваю), последнее - это то, что мы можем найти правильно перед началом массива из 11 элементов (a size_t со значением 11).

Доступ к памяти до начала массива - это естественное поведение undefined, но я сделал это, чтобы выставить некоторые детали реализации в gcc/clang. Дело в том, что они запросили дополнительные 8 байтов (как и было предсказано), и они действительно сохранили значение 11 там (размер массива).

Если вы измените этот 11 на 2, вызов delete[] фактически удалит неправильное количество элементов.

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

Или, теоретически, компилятор мог бы создать отдельную карту указателя- > размера.

Я не знаю компиляторов, которые делают это, но не удивятся ни тем, ни другим.

Разрешение этой техники - это то, о чем говорит стандарт С++. Для распределения массива код компилятора new (не operator new) разрешается запрашивать operator new для дополнительной памяти. Для распределения без массива компилятору new не разрешается запрашивать operator new для дополнительной памяти, он должен запросить точную сумму. (Я считаю, что могут быть исключения для слияния памяти?)

Как вы можете видеть, эти две ситуации различны.

Ответ 3

Нет противоречия между двумя параграфами.

В параграфе Стандарта обсуждаются правила первого аргумента, передаваемого функции распределения.

В абзаце из Stroustrup не говорится о первом аргументе, имеющем тип std:: size_t, но объясняет само распределение, которое является "двумя или более словами" больше, чем указывает новое, и что каждый программист должен знать.

Объяснение Stroustrup более низкое, это различие. Но нет противоречия.

Ответ 4

Цитата из стандарта говорит о значении, переданном оператору new; цитата из Stroustrup говорит о том, что новый оператор делает со значением. Эти два довольно независимы; требование заключается только в том, что распределитель выделяет не менее столько же памяти, сколько было запрошено. Распределители часто выделяют больше места, чем было запрошено. То, что они делают с этим дополнительным пространством, зависит от реализации; часто это просто дополнение. Обратите внимание, что даже если вы внимательно изучите требования, чтобы распределитель должен был указать точное количество запрошенных байтов, выделение большего разрешено в соответствии с правилом "как если", поскольку никакая портативная программа не может определить, сколько памяти было фактически выделено.

Ответ 5

Я не уверен, что оба говорят об одном и том же...

Кажется, что Stroustrup говорит о более общем распределении памяти, который по своей сути использует дополнительные данные для управления свободными/выделенными кусками. Я думаю, что он не говорит о значении размера, переданного new, но что действительно происходит на более низком уровне. Вероятно, он сказал бы: когда вы попросите 10 байт, машина, вероятно, будет использовать чуть более 10 байт. , используя стандартную реализацию, здесь важна.

В то время как стандарт говорит о значении, переданном функции.

Один говорит об осуществлении, а другой - нет.

Ответ 6

Нет противоречия, потому что "именно размер объекта" является одной возможной реализацией "как минимум, размера объекта".

Число 42 составляет не менее 42.