Реализация макроса __is_constexpr (ICE_P) ядра Linux на чистом C++

После прочитав стандартную версию C11 предиката ICE_P Мартина Юкера, я попытался реализовать его на чистом C++. Версия C11, использующая выбор _Generic, выглядит следующим образом:

#define ICE_P(x) _Generic((1? (void *) ((x)*0) : (int *) 0), int*: 1, void*: 0)

Очевидным подходом для C++ является замена _Generic шаблоном и decltype, например:

template<typename T> struct is_ice_helper;
template<> struct is_ice_helper<void*> { enum { value = false }; };
template<> struct is_ice_helper<int*>  { enum { value = true  }; };

#define ICE_P(x) (is_ice_helper<decltype(1? (void *) ((x)*0) : (int *) 0)>::value)

Однако он не проходит самый простой тест. Почему он не может обнаружить целочисленные константные выражения?


person StoryTeller - Unslander Monica    schedule 21.04.2019    source источник


Ответы (1)


Проблема тонкая. Спецификация для определения составного типа операндов-указателей условного выражения в C++ аналогична спецификациям в C, поэтому сначала она выглядит многообещающе:

(N4659) [expr.cond]

7 Lvalue-to-rvalue, массив-указатель , а стандартные преобразования функции в указатель выполняются для второго и третьего операндов. После этих преобразований должно выполняться одно из следующих условий:

  • [...]

  • Один или оба из второго и третьего операндов имеют тип указателя; преобразования указателя, преобразования указателя функции и преобразования квалификации выполняются, чтобы привести их к составному типу указателя (пункт [expr]). Результат имеет тип составного указателя.

  • [...]

Приведение к составному типу указателя определяется следующим образом:

(N4659) [выражение]

5 Тип составного указателя двух операндов p1 и p2, имеющих типы T1. и T2 соответственно, где хотя бы один из них является указателем или указателем на тип члена или std​::​nullptr_­t, это:

  • если и p1, и p2 являются константами нулевого указателя, std​::​nullptr_­t;
  • если p1 или p2 является константой нулевого указателя, T2 или T1 соответственно;
  • если T1 или T2 — «указатель на cv1 void», а другой тип — «указатель на cv2 T», где T — объектный тип или void, «указатель на cv12 void», где cv12 — объединение cv1 и cv2;
  • [...]

Таким образом, результат нашего макроса ICE_P определяется тем, какие из вышеперечисленных пуль мы попадаем после проверки каждой по порядку. Учитывая, как мы определили is_ice_helper, мы знаем, что составной тип не nullptr_t, иначе мы попали бы в первый пункт и получили бы ошибку из-за отсутствия специализации шаблона. Таким образом, мы, должно быть, попали в пункт номер 3, сделав отчет предиката ложным. Все это, кажется, зависит от определения константы нулевого указателя.

(N4659) [conv.ptr] (выделено мной)

1 константа нулевого указателя целочисленный литерал с нулевым значением или значением prvalue типа std​::​nullptr_­t. Константа нулевого указателя может быть преобразована в тип указателя; результатом является нулевое значение указателя этого типа, и его можно отличить от любого другого значения типа указателя объекта или указателя функции. Такое преобразование называется преобразованием нулевого указателя. Два нулевых значения указателя одного и того же типа должны сравниваться как равные. Преобразование константы нулевого указателя в указатель на тип cv-qualified является одиночным преобразованием, а не последовательностью преобразования указателя, за которым следует преобразование уточнения. Константа нулевого указателя целочисленного типа может быть преобразована в prvalue типа std​::​nullptr_­t.

Поскольку (int*)0 не является константой нулевого указателя в соответствии с приведенным выше определением, мы не можем претендовать на первый маркер [expr]/5. Составной тип не std::nullptr_t. (void *) ((x)*0) не является константой нулевого указателя и не может быть преобразована в нее. Удаление слепка (чего не позволяет определение) оставляет нас с (x)*0. Это целочисленное константное выражение с нулевым значением. Но это не целочисленный литерал с нулевым значением! Определение константы нулевого указателя в C++ отличается от определения в C!

(N1570) 6.3.2.3 Указатели

3 Целочисленное константное выражение со значением 0, или такое выражение, приведенное к типу void *, называется константой нулевого указателя. Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно будет сравниваться с неравным указателю на любой объект или функцию.

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

person StoryTeller - Unslander Monica    schedule 21.04.2019
comment
Я думаю, что ограничение может быть связано с тем, что 1 - 1 не должно быть константой нулевого указателя. Не знаю, почему они решили запретить (int*)0 быть константой нулевого указателя. - person Rakete1111; 21.04.2019
comment
@ Rakete1111 - Что касается 1 - 1. Формулировка была изменена в период с 2003 по 2011 год (см. n1905 §4.10). Так что это кажется преднамеренным, и я хотел бы знать мотивацию (не смог ее найти). Что касается (int*), в этом есть смысл. Единственная полезная константа нулевого указателя — это сортировка, которая может быть преобразована в любой тип указателя, и (int*)0 не подходит. - person StoryTeller - Unslander Monica; 21.04.2019
comment
@ Rakete1111 - я бы сказал, что не могу найти его за мгновение до того, как смогу... CWG 903. Но я не могу не чувствовать, что резолюция как бы выплеснула ребенка вместе с водой из ванны. - person StoryTeller - Unslander Monica; 21.04.2019
comment
В (int*)0 является ли 0 константой нулевого указателя, которая преобразуется приведением в нулевой указатель на значение int (но не константа)? Кажется... вода в ванне. - person Peter - Reinstate Monica; 27.08.2020
comment
@Питер - Ага. 0 — это константа нулевого указателя, преобразованная в значение нулевого указателя int*. Но (int*)0 в целом не является константой нулевого указателя, несмотря на то, что является постоянным выражением. Это почти забавно. - person StoryTeller - Unslander Monica; 27.08.2020