Ответ 1
Я считаю, что поведение Clang и MSVC согласуется со стандартом в этой ситуации. Я думаю, что GCC здесь немного сокращается.
Положим сначала несколько фактов на стол. Операндом выражения decltype
является то, что называется неоцененным операндом, который обрабатывается несколько иначе из-за того, что они в конечном счете никогда не оцениваются.
В частности, существует меньше требований к завершению типов. В принципе, если у вас есть временный объект (в качестве параметров или возвращаемых значений в функциях или операторах, участвующих в выражении), они не обязательно должны быть полными (см. Разделы 5.2.2/11 и 7.1.6.2/5). Но это только отменяет обычное ограничение "вы не можете объявить объект неполного типа", но оно не отменяет другого ограничения на неполные типы, что означает, что "вы не можете вызвать функцию-член неполного типа". И это кикер.
Выражение decltype(T())
или decltype(T{})
, где T
является неполным, обязательно должно искать конструктор типа T
, поскольку он (специальная) функция-член этого класса. Это только тот факт, что вызов конструктора создает немного неоднозначности (т.е. Просто создает временный объект или вызывает вызов конструктора?). Если бы это была какая-либо другая функция-член, дискуссий не было бы. К счастью, стандарт разрешает эту дискуссию:
12.2/1
Даже если создание временного объекта не оценено (пункт 5) или иным образом избегается (12.8), все семантические ограничения должны следует уважать, как если бы временный объект был создан, а затем уничтожены. [Примечание: даже если нет вызова деструктора или копировать/перемещать конструктор, все семантические ограничения, такие как доступность (раздел 11) и удалена ли функция (8.4.3). Однако в частном случае вызов функции, используемый как операнд спецификатора decltype (5.2.2), no введено временное, поэтому вышеизложенное не применяется к prvalue любого такого вызова функции. - конечная нота]
Последнее предложение может быть немного запутанным, но оно применимо только к возвращаемому значению вызова функции. Другими словами, если у вас есть функция T f();
, и вы объявляете decltype(f())
, то T
не требуется быть полным или иметь какие-либо семантические проверки на наличие и доступность для него конструктора/деструктора.
На самом деле, вся эта проблема именно поэтому есть утилита std::declval
, потому что, когда вы не можете использовать decltype(T())
, вы можете просто использовать decltype(std::declval<T>())
, а declval
- не более чем функция (фальшивка) который возвращает значение типа T
. Но, конечно, declval
предназначен для использования в менее тривиальных ситуациях, таких как decltype( f( std::declval<T>() ) )
, где f
будет функцией, принимающей объект типа T
. И declval
не требует, чтобы тип был полным (см. Раздел 20.2.4). Это в основном то, как вы справляетесь со всей этой проблемой.
Итак, что касается поведения GCC, я считаю, что он требует короткого замыкания, поскольку пытается выяснить, что такое тип T()
или T{}
. Я думаю, что, как только GCC обнаружит, что T
ссылается на имя типа (а не на имя функции), он выводит, что это вызов конструктора, и, следовательно, независимо от того, что поиск находит в качестве вызываемого фактического конструктора, тип возврата будет T
(ну, строго говоря, конструкторы не имеют типа возврата, но вы понимаете, что я имею в виду). Дело здесь в том, что это может быть полезным (более быстрым) сокращением в неоценимом выражении. Насколько мне известно, это не стандартное поведение.
И если GCC разрешает CompleteType
с конструктором, либо удаленным, либо приватным, то это также прямо противоречит вышеприведенному проходу стандарта. Компилятор должен обеспечивать соблюдение всех семантических ограничений в этой ситуации, даже если выражение не оценивается.
Обратите внимание, что простая строка типа
std::cout << typeid(X(IncompleteType)).name() << std::endl;
не компилируется для всех компиляторов для всех вариантов X (кроме vС++ и X (T) == T).
Ожидается (за исключением MSVC и X (T) == T). Операторы typeid
и sizeof
аналогичны decltype
в том смысле, что их операнды не оценены, однако оба они имеют дополнительное требование о том, что тип результирующего выражения должен быть полным типом. Можно предположить, что компилятор может разрешить typeid
для неполных типов (или, по крайней мере, с частичной информацией типа), но для этого стандарта требуется полный тип, так что компиляторы не должны этого делать. Я думаю, это то, что делает MSVC.
Итак, в этом случае случаи T()
и T{}
выполняются по той же причине, что и для decltype
(как я только что объяснил), и случай X(T) == T
завершается с ошибкой, потому что typeid
требуется полный тип (но MSVC удается снять это требование). И на GCC он терпит неудачу из-за typeid
, требующего полного типа для всех случаев X(T)
(т.е. Короткое сокращение GCC не влияет на результат в случае sizeof
или typeid
).
Итак, в целом, я думаю, что Clang является самым стандартным из трех (не снимая сокращений или расширений).