В чем причина возврата уникальных адресов для выделения нулевого размера в C++?

В чем причина возврата уникальных адресов для выделения нулевого размера в C++?

Предпосылки: стандарт C11 говорит о malloc (7.20.3 Функции управления памятью):

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

То есть, как я вижу, malloc всегда преуспевает для выделения нулевого размера, поскольку единственное, что вы можете сделать с указателем нулевого размера, - это вызов другой функции выделения памяти, такой как free с ней:

  • если malloc возвращает NULL, free(NULL) в порядке, так что это можно считать успешным,
  • если он возвращает какое-то другое значение, это также приводит к успеху (потому что это не NULL), единственным условием является то, что free от значения также должен работать.

Кроме того, C11 (также 7.20.3) не указывает, что адрес, возвращаемый из malloc, должен быть уникальным, только чтобы они указывали на непересекающиеся области памяти:

Указатель возвращается, если выделение успешно выполняется соответствующим образом, чтобы его можно было назначить указателю на любой тип объекта, а затем использовать для доступа к такому объекту или массиву таких объектов в выделенном пространстве (пока пространство явно не освобождено), Время жизни выделенного объекта простирается от выделения до освобождения. Каждое такое распределение должно давать указатель на объект, не связанный с каким-либо другим объектом.

Все объекты нулевого размера являются непересекающимися AFAICT, и это будет означать, что malloc может возвращать один и тот же указатель для множественных распределений нулевого размера (например, NULL будет в порядке) или разных указателей каждый раз или одного и того же указателя для некоторых и т.д.

Затем C++ 98 объединил две функции распределения памяти:

void* operator new(std::size_t size);
void* operator new(std::size_t size, std::align_val_t alignment);

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

Вы называете их следующим образом:

#include <iostream>
#include <new>
int main() {
    void* ptr = operator new(std::size_t{0});
    std::cout << ptr << std::endl;
    operator delete(ptr, std::size_t{0});
    return 0;
}

Раздел [new.delete.single] в стандарте C++ 17 объясняет их, но гарантия ключа, как я вижу, приведена в [basic.stc.dynamic.allocation]:

Даже если размер запрашиваемого пространства равен нулю, запрос может выйти из строя. Если запрос завершается успешно, возвращаемое значение должно быть значением ненулевого указателя (7.11) p0, отличным от любого ранее возвращенного значения p1, если только это значение p1 не было впоследствии передано удалению оператора. Кроме того, для функций распределения библиотек в пунктах 21.6.2.1 и 21.6.2.2 p0 должен представлять адрес блока хранения, не связанного с хранилищем, для любого другого объекта, доступного для вызывающего. Эффект косвенности через указатель, возвращаемый как запрос на нулевой размер, не определен.

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

Мой вопрос: в чем причина этого изменения? (то есть за возвратом уникальных адресов для выделения нулевого размера в C++)

В идеале ответ был бы просто ссылкой на документ (или какой-то другой источник), который изучал альтернативы и мотивировал их семантику. Обычно я обращаюсь к Design and Evolution C++ для этих C++ 98 вопросов, но в Разделе 10 (Управление памятью) ничего об этом не упоминается. В противном случае какая-то авторитетная ссылка была бы приятной.


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

Кроме того, на reddit люди продолжали и продолжали примерно нулевые размеры, есть ли у меня предложение изменить стандарт и т.д. Этот вопрос касается семантики функций выделения необработанных памяти при передаче размера, равного нулю. Если для вашего ответа важны темы типа нулевого размера, пожалуйста, включите их! Но постарайтесь не срываться с тангенциальными проблемами.

Кроме того, на reddit люди также высказывали такие аргументы, как "что для целей оптимизации", не имея возможности упомянуть ничего более конкретного. Я бы ожидал чего-то более конкретного, чем "из-за оптимизации" в ответе. Например, один redditor упомянул оптимизацию псевдонимов, но я задавался вопросом, какие из оптимизаций псевдонимов применяются к указателям, которые не могут быть разыменованы, и не могли заставить кого-либо прокомментировать это. Поэтому, возможно, если вы собираетесь упомянуть об оптимизации, небольшой пример, который показывает это, обогатит обсуждение.

Ответ 1

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

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


Многие комментарии о проблеме "новый не возвращают объекты".

Пожалуйста, ЗАКЛЮЧИТЕ терминологию ООП в этом контексте:

Спецификация C++ имеет точное определение того, что означает слово "объект".

Ссылка CPP: Объект

Особенно:

C++ программы создают, уничтожают, ссылаются, получают доступ и манипулируют объектами. Объектом в C++ является область хранения, которая имеет

  • размер (может быть определен с помощью sizeof);
  • требование выравнивания (может быть определено с помощью выравнивания);
  • длительность хранения (автоматическая, статическая, динамическая, нить-локальная);
  • время жизни (ограниченное длительностью хранения или временным);
  • тип;
  • значение (которое может быть неопределенным, например, для инициализированных по умолчанию типов неклассов);
  • необязательно, имя.

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

Переменная - это объект или ссылка, которая не является элементом нестатического данных, который вводится декларацией.

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

Ответ 2

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

Однако ваш вопрос предполагает, что это изменение. Помимо краткого перерыва в конце 1980 года все реализации C и C++, о которых я знаю, всегда вели себя следующим образом.

Первоначальный компилятор C от dmr вел себя так, но примерно в 1987 году в проекте стандарта C указано, что malloc объекта нулевого размера возвращает NULL. Это было действительно странно, и даже окончательный стандарт C-89 сделал его реализацией, но я никогда не сталкивался с реализацией, которая сделала это ужасно.

Об этом я больше об этом расскажу в своем блоге в разделе "Malloc Madness".