Ответ 1
Для С++ 14 и 11 Clang прав; однако в последнем рабочем проекте (будущее С++ 17) все изменилось - см. следующий раздел.
Стандартные котировки для поиска (от N4140, проект ближе всего к С++ 14):
[temp.inst]/1:
[...] Неявное инстанцирование специализации шаблона класса вызывает неявное создание объявлений, но не определения, аргументы по умолчанию или спецификации исключения функции-члены класса, классы-члены, узкоспециализированные перечисления, статические элементы данных и шаблоны членов; [...]
[temp.point]/4:
Для специализации шаблона класса [...] точка инстанцирования поскольку такая специализация сразу предшествует области пространства имен декларация или определение, относящееся к специализации.
Итак, точка инстанцирования для S<U>
находится прямо перед объявлением U
, только с передним объявлением struct U;
, предварительно введенным ранее, так что будет найдено имя U
.
[class.static.data]/3:
[...] Статический член данных типа literal может быть объявлен в определение класса с помощью спецификатора
constexpr
; если да, то в декларации указывается логический или равный-инициализатор, в котором каждое предложение-инициализатор, являющееся выражением присваивания, является постоянное выражение. [...] Член должен быть определен в область пространства имен, если она используется в odr (3.2) в программе и определение области пространства имен не должно содержать инициализатор.
В соответствии с приведенным выше параграфом декларация bar
в определении S
, даже если она имеет инициализатор, по-прежнему является просто объявлением, а не определением, поэтому она создается, когда S<U>
неявно экземпляр, и там нет U::foo
в это время.
Обходной путь состоит в том, чтобы сделать bar
функцией; согласно первой цитате, определение функции не будет создаваться во время неявного экземпляра S<U>
. Пока вы используете bar
после того, как было определено определение U
(или изнутри тел других функций-членов S
), поскольку они, в свою очередь, будут создаваться отдельно отдельно, когда это необходимо - [14.6. 4.1p1]), что-то вроде этого будет работать:
template<class T> struct S
{
static constexpr int bar() { return T::foo; }
};
struct U : S<U> { static constexpr int foo = 42; };
int main()
{
constexpr int b = U::bar();
static_assert(b == 42, "oops");
}
После принятия P0386R2 в рабочий проект (в настоящее время N4606), [class.static.data]/3 были изменены; соответствующая часть теперь читает:
[...] Встроенный элемент статических данных может быть определен в определении класса и может указывать логический или равный-инициализатор. Если член объявленный с помощью спецификатора
constexpr
, он может быть переоформлен в область пространства имен без инициализатора (это использование устарело, см. D.1). [...]
Это дополняется изменением на [basic.def]/2.3:
Декларация - это определение, если: [...]
- он объявляет не-встроенный элемент статических данных в определении класса (9.2, 9.2.3),
[...]
Итак, если он встроен, это определение (с инициализатором или без него). И [dcl.constexpr]/1 говорит:
[...] Функция или элемент статических данных, объявленный с помощью
constexpr
Спецификатор неявно является встроенной функцией или переменной (7.1.6). [...]
Это означает, что объявление bar
теперь является определением, и в соответствии с кавычками в предыдущем разделе он не создается для неявного экземпляра S<U>
; в это время создается только объявление bar
, которое не включает инициализатор.
Изменения в этом случае хорошо представлены в примере в [des.static_constexpr] в текущем рабочем черновике:
struct A {
static constexpr int n = 5; // definition (declaration in C++ 2014)
};
const int A::n; // redundant declaration (definition in C++ 2014)
Это делает поведение GCC стандартно-совместимым в режиме С++ 1z.