Вот решение проще, чем
Йоханнес Шауб - лит х один . Требуется C ++ 11.
#include <type_traits>
template <typename T, typename = int>
struct HasX : std::false_type { };
template <typename T>
struct HasX <T, decltype((void) T::x, 0)> : std::true_type { };
Обновление : быстрый пример и объяснение того, как это работает.
Для этих типов:
struct A { int x; };
struct B { int y; };
у нас есть HasX<A>::value == true
и HasX<B>::value == false
. Посмотрим почему.
Сначала напомним, что std::false_type
и std::true_type
имеют член static constexpr bool
с именем value
, для которого установлены false
и true
соответственно. Следовательно, два шаблона HasX
выше наследуют этот элемент. (Первый шаблон от std::false_type
и второй от std::true_type
.)
Давайте начнем с простого, а затем продолжим шаг за шагом, пока не дойдем до кода выше.
1) Начальная точка:
template <typename T, typename U>
struct HasX : std::false_type { };
В этом случае нет ничего удивительного: HasX
происходит от std::false_type
и, следовательно, HasX<bool, double>::value == false
и HasX<bool, int>::value == false
.
2) По умолчанию U
:
// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };
Учитывая, что U
по умолчанию равно int
, Has<bool>
фактически означает HasX<bool, int>
и, таким образом, HasX<bool>::value == HasX<bool, int>::value == false
.
3) Добавление специализации:
// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };
// Specialization for U = int
template <typename T>
struct HasX<T, int> : std::true_type { };
Как правило, благодаря основному шаблону HasX<T, U>
происходит от std::false_type
. Однако существует специализация для U = int
, которая происходит от std::true_type
. Следовательно, HasX<bool, double>::value == false
, но HasX<bool, int>::value == true
.
Благодаря стандартному значению U
, HasX<bool>::value == HasX<bool, int>::value == true
.
4) decltype
и причудливый способ сказать int
:
Небольшое отступление, но, пожалуйста, потерпите меня.
В основном (это не совсем правильно), decltype(expression)
возвращает тип выражения . Например, 0
имеет тип int
, таким образом, decltype(0)
означает int
. Аналогично, 1.2
имеет тип double
и, таким образом, decltype(1.2)
означает double
.
Рассмотрим функцию с этим объявлением:
char func(foo, int);
, где foo
- некоторый тип класса. Если f
является объектом типа foo
, то decltype(func(f, 0))
означает char
(тип, возвращаемый func(f, 0)
).
Теперь выражение (1.2, 0)
использует (встроенный) оператор запятой, который вычисляет два подвыражения по порядку (то есть сначала 1.2
, а затем 0
), отбрасывает первое значение и приводит к второй. Следовательно,
int x = (1.2, 0);
эквивалентно
int x = 0;
Если поставить это вместе с decltype
, получится, что decltype(1.2, 0)
означает int
. Здесь нет ничего особенного в 1.2
или double
. Например, true
имеет тип bool
, а decltype(true, 0)
также означает int
.
А как насчет типа класса? Для instace, что означает decltype(f, 0)
? Естественно ожидать, что это все еще означает int
, но это может быть не так. Действительно, может быть перегрузка для оператора запятой, аналогичного функции func
выше, которая принимает foo
и int
и возвращает char
. В этом случае decltype(foo, 0)
равно char
.
Как мы можем избежать использования перегрузки для оператора запятой? Ну, нет никакого способа перегрузить оператор запятой для операнда void
, и мы можем привести все к void
. Следовательно, decltype((void) f, 0)
означает int
. В самом деле, (void) f
приводит f
к foo
к void
, что в основном ничего не говорит, но говорит о том, что выражение должно рассматриваться как имеющее тип void
. Затем используется встроенная запятая оператора и ((void) f, 0)
приводит к 0
, который имеет тип int
. Следовательно, decltype((void) f, 0)
означает int
.
Действительно ли этот бросок необходим? Ну, если нет перегрузки для оператора запятой, принимающего foo
и int
, то в этом нет необходимости. Мы всегда можем проверить исходный код, чтобы увидеть, есть ли такой оператор или нет. Однако, если это появляется в шаблоне и f
имеет тип V
, который является параметром шаблона, то уже не ясно (или даже невозможно узнать), существует ли такая перегрузка для оператора запятой или нет. Чтобы быть универсальным, мы все равно разыгрываем.
Итог: decltype((void) f, 0)
- причудливый способ сказать int
.
5) СФИНА:
Это целая наука ;-) Хорошо, я преувеличиваю, но это не очень просто. Поэтому я сведу объяснение к минимуму.
SFINAE означает «Ошибка замены не является ошибкой». Это означает, что когда параметр шаблона заменяется типом, может появиться недопустимый код C ++, но в некоторых случаях вместо прерывания компиляции компилятор просто игнорирует код, вызывающий сбой, как если бы его там не было. Давайте посмотрим, как это относится к нашему случаю:
// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };
// Specialization for U = int
template <typename T>
struct HasX <T, decltype((void) T::x, 0)> : std::true_type { };
Здесь, опять же, decltype((void) T::x, 0)
- причудливый способ сказать int
, но с пользой для СФИНА.
Когда T
заменяется типом, может появиться недопустимая конструкция. Например, bool::x
не является допустимым C ++, поэтому замена T
на bool
в T::x
приводит к неверной конструкции. Согласно принципу SFINAE, компилятор не отклоняет код, он просто игнорирует его (части). Точнее, как мы видели, HasX<bool>
означает на самом деле HasX<bool, int>
. Специализация для U = int
должна быть выбрана, но при ее создании компилятор находит bool::x
и полностью игнорирует специализацию шаблона, как если бы он не существовал.
На этом этапе код, по сути, такой же, как и в случае (2) выше, где существует только основной шаблон. Следовательно, HasX<bool, int>::value == false
.
Тот же аргумент, что и для bool
, сохраняется и для B
, поскольку B::x
является недопустимой конструкцией (B
не имеет члена x
). Тем не менее, A::x
в порядке, и компилятор не видит проблем в создании специализации для U = int
(или, точнее, для U = decltype((void) A::x, 0)
). Следовательно, HasX<A>::value == true
.
6) Unnaming U
:
Итак, снова посмотрев на код в (5), мы увидим, что имя U
не используется нигде, кроме его объявления (typename U
). Затем мы можем отменить имя второго аргумента шаблона и получить код, показанный в верхней части этого поста.