Неполные типы кода шаблона

Предположим, что у нас есть два типа (полный и неполный):

struct CompleteType{};

struct IncompleteType;

Также у нас есть код шаблона:

#include <type_traits>

template <typename = X(T)>
struct Test : std::false_type {};

template <>
struct Test<T> : std::true_type {};

T может быть CompleteType или IncompleteType здесь, а X(T) может быть T, decltype(T()) или decltype(T{}) (предположим, что X(T) - макрос).

Этот код используется следующим образом:

std::cout << std::boolalpha << Test<>::value << std::endl;

Ниже вы можете увидеть, как разные компиляторы имеют дело с таким кодом:


clang 3.4

X(T) \ T       CompleteType  IncompleteType
T              true          true      
decltype(T())  true          --- (1, 2)
decltype(T{})  true          --- (1, 2)
  • error: invalid use of incomplete type 'IncompleteType' предоставляется даже в объявлениях классов классов с неполными типами (как для decltype(T()), так и decltype(T{}), но не для простого T) без использования Test<>::value в коде.

  • error: too few template arguments for class template 'Test'


g++ 4.8.1

X(T) \ T       CompleteType  IncompleteType
T              true          true      
decltype(T())  true          true      
decltype(T{})  true          true      

vС++ 18.00.21005.1

X(T) \ T       CompleteType  IncompleteType
T              true          true      
decltype(T())  true          --- (1)   
decltype(T{})  true          --- (2)   
  • error C2514: 'IncompleteType' : class has no constructors

  • error C2440: '<function-style-cast>' : cannot convert from 'initializer-list' to 'IncompleteType' Source or target has incomplete type


Какой компилятор действует в соответствии со стандартом? Обратите внимание, что простая строка типа std::cout << typeid(X(IncompleteType)).name() << std::endl; не компилируется для всех компиляторов для всех вариантов X (кроме vС++ и > X(T) == T).

Ответ 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 является самым стандартным из трех (не снимая сокращений или расширений).