Вот более короткое воспроизведение, рассмотрим разницу между программой, скомпилированной с ACCEPT
, и программой без:
struct One { constexpr operator int() const { return 1; } };
template <typename T>
constexpr int foo(T&& t) {
#ifdef ACCEPT
return t;
#else
constexpr int i = t;
return i;
#endif
}
constexpr int i = foo(One{});
Как можно предположить при выборе макроса, случай ACCEPT
в порядке, идругой случай плохо сформирован.Почему?Рассматриваемое правило: [expr.const] /4.12:
Выражение e
является основным константным выражением , если только оценка e
, следуя правилам абстрактной машины, оценил бы одно из следующего: [...] id-выражение , которое ссылается на переменную или член данных ссылочного типа, если ссылка не имеет предшествующей инициализациии либо [...]
Что такое до инициализации ?Прежде чем я отвечу, давайте предоставим другую программу и пройдемся по ее семантике:
Противоречие
struct Int { constexpr operator int() const { return i; } int i; };
template <int> struct X { };
template <typename T>
constexpr auto foo(T&& t) {
constexpr int i = t;
return X<i>{};
}
constexpr auto i = foo(Int{1});
constexpr auto j = foo(Int{2});
Есть только одна функция foo<Int>
, поэтому онадолжен иметь один конкретный тип возврата.Если бы эта программа была разрешена, то foo(Int{1})
вернул бы X<1>
, а foo(Int{2})
вернул бы X<2>
- то есть foo<Int>
может вернуть разные типы?Этого не может быть, поэтому это должно быть неправильно сформировано.
Наименьшее возможное поле
Когда мы находимся в ситуации, требующей постоянного выражения, воспринимайте это как открытие нового окна,Все в этом поле должно соответствовать правилам постоянной оценки, как если бы мы только начали с этого момента.Если нам требуется новое константное выражение, вложенное в этот блок, мы открываем новый блок.Ящики полностью вниз.
И в исходном воспроизведении (с One
), и в новом воспроизведении (с Int
) у нас есть это объявление:
constexpr int i = t;
Это открываетновая коробка.Инициализатор t
должен удовлетворять ограничениям константных выражений.t
является ссылочным типом, но не имеет предшествующей инициализации в этом поле , следовательно, оно некорректно сформировано.
Теперь в принятом случае:
struct One { constexpr operator int() const { return 1; } };
template <typename T>
constexpr int foo(T&& t) {
return t;
}
constexpr int i = foo(One{});
У нас есть только одно поле: инициализация глобальной i
.Внутри этого блока мы по-прежнему оцениваем id-выражение ссылочного типа в пределах этого return t;
, но в этом случае у нас do есть предшествующая инициализация внутри нашего блока: у нас естьвидимость, где мы связываем t
с One{}
.Так что это работает.Нет противоречия, которое можно построить из этих правил.В самом деле, это тоже было бы хорошо:
constexpr int j = foo(Int{1});
constexpr int k = foo(Int{2});
static_assert(i+k == 3);
Поскольку у нас все еще просто один вход каждый раз в постоянную оценку, и в этой оценке ссылка t
имеет предшествующую инициализацию, и члены Int
также можно использовать в константных выражениях.
Назад к OP
Удаление ссылки работает, потому что мы больше не нарушаем ограничение ссылки, и нет никаких других ограничений, которые мы могли бы нарушить.Мы не читаем ни одно переменное состояние или что-то еще, функция преобразования просто возвращает константу.
Аналогичный пример, в котором мы пытались передать Int{1}
в foo
по значению, все равно не получится - на этот раз не для ссылочного правила, а вместо этого для правила преобразования lvalue-to-rvalue.По сути, мы читаем что-то, что нам нельзя разрешить читать - потому что мы в конечном итоге столкнулись с тем же противоречием, состоящим в возможности создания функции с несколькими типами возвращаемых значений.