Почему С++ 11 содержит странное предложение о сравнении пустых указателей?

Проверяя ссылки на другой вопрос, я заметил странное предложение в С++ 11, в [expr.rel] ¶3:

Указатели на void (после преобразования указателей) можно сравнить с результатом, определенным следующим образом: если оба указателя представляют один и тот же адрес или оба являются нулевыми значениями указателя, результатом будет true если оператор равен <= или >= и false противном случае.; в противном случае результат не уточняется.

Похоже, это означает, что после того, как два указателя были void *, их порядок упорядочения больше не гарантируется; например, это:

int foo[] = {1, 2, 3, 4, 5};
void *a = &foo[0];
void *b = &foo[1];
std::cout<<(a < b);

казалось бы, не определено.

Интересно, что этот пункт отсутствовал в С++ 03 и исчез в С++ 14, поэтому, если мы возьмем приведенный выше пример и применим к нему формулировку С++ 14, я бы сказал, что ¶3.1

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

будет применяться, так как a и b указывают на элементы одного и того же массива, даже если они были преобразованы в void *. Обратите внимание, что формулировка ¶3.1 была примерно такой же в С++ 11, но, похоже, была отвергнута предложением void *.

Правильно ли я понимаю? Какой смысл в этом странном предложении, добавленном в С++ 11 и сразу удаленном? Или, может быть, он все еще там, но перенесен/подразумевается какой-то другой частью стандарта?

Ответ 1

TL; DR:

  • в С++ 98/03 этого условия не было, и стандарт не определял реляционные операторы для void указателей (основной вопрос 879, см. конец этого поста);
  • странное предложение о сравнении void указателей было добавлено в С++ 11 для его решения, но это, в свою очередь, привело к двум другим основным проблемам 583 и 1512 (см. ниже);
  • решение этих вопросов потребовало удалить пункт и заменить его формулировкой, найденной в стандарте С++ 14, которая допускает "нормальное" сравнение void *.

Основная проблема 583. Сравнение реляционных указателей с константой нулевого указателя

  1. Сравнение реляционного указателя с константой нулевого указателя Раздел: 8.9 [expr.rel]

В C это плохо сформировано (ср. C99 6.5.8):

void f(char* s) {
    if (s < 0) { }
} ...but in C++, it not. Why? Who would ever need to write (s > 0) when they could just as well write (s != 0)?

Это было на языке начиная с ARM (и, возможно, раньше); очевидно, это потому, что преобразования указателей (7.11 [conv.ptr]) должны выполняться для обоих операндов, когда один из операндов имеет тип указателя. Таким образом, похоже, что преобразование "null-ptr-to-real-pointer-type" объединяет другие преобразования указателей.

Предлагаемое решение (апрель 2013 г.):

Эта проблема решена решением вопроса 1512.

Основная проблема 1512: сравнение указателей и преобразований квалификаций

  1. Сравнение указателей и преобразований квалификаций Раздел: 8.9 [expr.rel]

В соответствии с пунктом 8.9 [expr.rel], описывающим сравнения указателей,

Преобразования указателя (7.11 [conv.ptr]) и преобразования квалификации (7.5 [conv.qual]) выполняются над операндами указателя (или над операндом указателя и константой нулевого указателя, или над двумя константами нулевого указателя, по крайней мере, одна из которых не является интегральным), чтобы привести их к их составному типу указателя. Это может привести к тому, что следующий пример будет плохо сформирован,

 bool foo(int** x, const int** y) {
 return x < y;  // valid ?   } because int** cannot be converted to const int**, according to the rules of 7.5 [conv.qual] paragraph 4.

Это кажется слишком строгим для сравнения указателей, и текущие реализации принимают пример.

Предлагаемое решение (ноябрь 2012 г.):


Соответствующие выдержки из решения вышеупомянутых проблем находятся в документе: Сравнение указателей и преобразований квалификаций (редакция 3).

Следующее также решает основную проблему 583.

Изменение в 5.9 expr.rel пунктах 1-5:

В этом разделе было удалено следующее утверждение (нечетное предложение в С++ 11):

Указатели на void (после преобразования указателей) можно сравнить с результатом, определенным следующим образом: если оба указателя представляют один и тот же адрес или оба являются нулевыми значениями указателя, результатом будет true если оператор равен <= или >= и false противном случае.; в противном случае результат не указан

И следующие заявления были добавлены:

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

Таким образом, в окончательном рабочем проекте раздела С++ 14 (n4140) [expr.rel]/3 приведенные выше утверждения находятся в том виде, в котором они были сформулированы во время принятия резолюции.


Копание по причине, по которой было добавлено это странное предложение, привело меня к гораздо более ранней проблеме 879: Отсутствие встроенных операторов сравнения для типов указателей. Предлагаемое решение этого вопроса (в июле 2009 года) привело к добавлению этого пункта, который был принят на рассмотрение WP в октябре 2009 года.

И вот как это было включено в стандарт С++ 11.

Ответ 2

Как в коммете подсказывает.

@SPlatten: любой указатель данных имеет неявное преобразование в void *, поэтому явное приведение не требуется, хотя верно, что на некоторых нечетных архитектурах (на ум приходит сегментированная память) приведение к void * не может быть простой побитовой копией; тем не менее, я не могу представить архитектуру, в которой преобразование "обычный указатель" в "большой, пустой указатель" не сохраняло бы отношение упорядочения между элементами одного и того же массива.

Ответ 3

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

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

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

Давай конкретно сосредоточимся на

... или подобъектам

Давайте возьмем "объект может содержать другие объекты, которые называются подобъектами. Они включают в себя объекты-члены" как определение для "подобъекта". Если принять вышеуказанное серьезно, это будет означать, что два указателя на два разных подобъекта одного и того же объекта будут представлять один и тот же индекс подписки, даже если "адреса памяти" различны, верно? Тем не менее, без знания размера объектов невозможно определить, указывают ли два указателя с разными значениями на один и тот же объект или нет.

И вот почему я думаю, что этот пункт

Указатели на void (после преобразования указателей) можно сравнить с результатом, определенным следующим образом: если оба указателя представляют один и тот же адрес или оба являются нулевыми значениями указателя, результатом будет true, если оператор равен <= или> =, и false в противном случае.; в противном случае результат не уточняется.

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

Чтобы проиллюстрировать это, рассмотрим следующий пример, в котором указатели сравниваются с двумя разными подобъектами одного и того же объекта; попробуйте - я не думаю, что какая-либо реализация могла бы правильно интерпретировать a < b соответствии с индексным отношением на основе подписки:

bool lessThan(void* a, void*b) {
    return a < b;
}

int main() {

    struct {
        int subobj1;
        int subobj2;
    } objects[5];

    void *subobj1_0 = &objects[0].subobj1;
    void *subobj2_0 = &objects[0].subobj2;

    bool shouldNotBeLessThan = lessThan(subobj1_0,subobj2_0);

    std::cout << "should it be 0 (i.e. not less than)? it is ... " << shouldNotBeLessThan << std::endl;
}

Ответ 4

Формулировка просто неверна, потому что она требует указателей на один и тот же объект или двух нулевых указателей для сравнения неравных с оператором ==.