Функция шаблона, которая соответствует только определенным типам?

Я хочу определить шаблон функции:

template<typename T>
void foo(T arg)

Но я хочу, чтобы T соответствовало только определенным типам. В частности, T должен быть производным (возможно, посредством множественного наследования) от определенного базового класса. В противном случае этот шаблон не должен быть включен в набор перегрузки.

Как я могу это сделать?


person becko    schedule 23.07.2015    source источник
comment
Связано с Как ограничить класс шаблона определенными типами?   -  person Shafik Yaghmour    schedule 23.07.2015
comment
@ShafikYaghmour Обратите внимание, что это не дубликат. Я хочу ограничить шаблон function, а связанный вопрос относится к шаблону class.   -  person becko    schedule 23.07.2015
comment
@ShafikYaghmour Разве вы не закрыли бы его, если бы это был точный дубликат, и у вас был ответ на другой?   -  person Barry    schedule 23.07.2015
comment
@ Барри, я не думаю, что это точная копия ... в качестве отступления я упомянул, что единственная причина, по которой я бы не использовал молоток для дублирования, - это если бы у меня был ответ на предложенный дубликат.   -  person Shafik Yaghmour    schedule 23.07.2015
comment
@ShafikYaghmour Почему бы и нет? Не хотите чувствовать, что вы играете в систему?   -  person Barry    schedule 23.07.2015
comment
@Barry, этот мета-вопрос сделал меня более консервативным в отношении того, как я его использую.   -  person Shafik Yaghmour    schedule 23.07.2015
comment
Ключевой вопрос для выбора наилучшего ответа заключается в следующем: если кто-то попытается использовать вашу функцию с неподходящим T, хотите ли вы неудачи или хотите, чтобы были рассмотрены другие перегрузки?   -  person Nir Friedman    schedule 24.07.2015
comment
@NirFriedman Я не хочу провала, я хочу, чтобы учитывались другие перегрузки. Должен ли я редактировать вопрос?   -  person becko    schedule 24.07.2015
comment
@becko Думаю, да, это сделает ваш вопрос более полезным для будущих читателей. Если это то, что вы хотите, то вам определенно нужен SFINAE, как объясняет ответ Барри; static_assert не удовлетворит ваши потребности. Если вас устраивает сбой, тогда static_assert проще и дает более качественные сообщения об ошибках.   -  person Nir Friedman    schedule 24.07.2015


Ответы (3)


Используйте SFINAE с std::is_base_of:

template <typename T,
          typename = std::enable_if_t<
              std::is_base_of<Foo, T>::value
          >>
void foo(T arg);

Это будет включать foo в набор перегрузки только в том случае, если T наследуется от Foo. Обратите внимание, что сюда входят также неоднозначные и недоступные базы. Если вам нужно решение, которое допускает только T, которые наследуют публично и однозначно от Foo, вместо этого вы можете использовать std::is_convertible:

template <typename T,
          typename = std::enable_if_t<
              std::is_convertible<T*, Foo*>::value
          >>
void foo(T arg);

Обратите внимание на перестановку аргументов.

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

template <typename T>
using enable_if_foo = std::enable_if_t<std::is_base_of<Foo, T>::value>;

template <typename T,
          typename = enable_if_foo<T>>
void foo(T arg);

Это работает, потому что std::enable_if имеет вложенный тип с именем type тогда и только тогда, когда логическое значение передано в true. Итак, если std::is_base_of<Foo, T>::value равно true, enable_if_t преобразуется в void, как если бы мы написали:

template <typename T,
          typename = void>
void foo(T arg);

Но, если T не наследуется от Foo, то признак типа будет оцениваться как false, а std::enable_if_t<false> будет ошибкой замены - typename enable_if<false>::type нет. Вы можете ожидать, что это приведет к ошибке компиляции, но sзамена failure is nне a n eошибка (sfinae). Это просто ошибка вывода шаблона. Таким образом, в этом случае foo<T> просто удаляется из набора жизнеспособных кандидатов на перегрузку, ничем не отличаясь от любой другой ошибки вывода шаблона.

person Barry    schedule 23.07.2015
comment
Я не нахожу std::enable_if_t. Должно быть std::enable_if? - person becko; 23.07.2015
comment
template<bool b, class T=void>using enable_if_t=typename std::enable_if<b,T>::type; — это однострочная реализация enable_if_t. Вставьте его в namespace notstd, и когда ваш компилятор обновится, переключите notstd::enable_if_t на std::enable_if_t. Псевдоним стоит дополнительного бита шаблона, синтаксис enable_if без него раздражает. - person Yakk - Adam Nevraumont; 23.07.2015
comment
Это то, что я хочу, спасибо (+1). Не могли бы вы объяснить, хотя бы поверхностно, как это работает? - person becko; 23.07.2015
comment
@Yakk Кроме того, я буду писать много функций, таких как foo, и все они должны совпадать, только если T наследуется от одного и того же базового класса. Есть ли сокращение, которое я могу использовать, typedef или что-то в этом роде, чтобы каждый раз не писать template<typename T, ... enable_if_t ... целиком? - person becko; 23.07.2015

Методы на основе SFINAE, такие как следующие:

template <typename T,
  typename Test = std::enable_if_t<std::is_base_of<Foo, T>::value>>
void foo(T arg);

Хорошо бы удалить функцию из списка перегрузок - это было бы общим случаем.

Если вы хотите сохранить функцию в списке, и если она выбрана как лучшая перегрузка, чтобы не сработать, если тип соответствует некоторым критериям (например, здесь базовое требование), можно использовать static_assert;

template <typename T>
void foo(T arg)
{
  static_assert(std::is_base_of<Foo, T>::value, "failed type check");
  // ...
}
person Niall    schedule 23.07.2015

В С++ 1z с облегченными концепциями вы можете сделать это:

template<class T>
requires std::is_base_of<Foo, T>{}()
void foo(T arg) {
}

в рамках текущей (экспериментальной) реализации. Что довольно чисто и ясно. может быть способ сделать что-то вроде:

template<derived_from<Foo> T>
void foo(T arg) {
}

но я не разобрался. Вы определенно можете сделать:

template<derived_from_foo T>
void foo(T arg){
}

где у нас есть пользовательская концепция под названием derived_from_foo, которая применяется, если тип получен из foo. Чего я не знаю, как делать, так это шаблонных понятий — понятий, сгенерированных из параметров типа шаблона.


В С++ 14 есть два метода. Во-первых, обычный SFINAE:

template<class T,
  class=std::enable_if_t<std::is_base_of<Foo, T>{}>
>
void foo(T arg) {
}

здесь мы создаем шаблон, который выводит тип T из своего аргумента. Затем он пытается вывести свой второй аргумент типа из первого аргумента.

Аргумент второго типа не имеет имени (отсюда class=), потому что мы используем его только для теста SFINAE.

Тест enable_if_t< condition >. enable_if_t< condition > генерирует тип void, если condition истинно. Если condition ложно, происходит сбой в "непосредственном контексте", что приводит к сбою замены.

SFINAE — это «Ошибка замены не является ошибкой» — если ваш тип T генерирует ошибку в «непосредственном контексте» сигнатуры шаблона функции, это не генерирует ошибку времени компиляции, а вместо этого приводит к тому, что шаблон функции не считается допустимой перегрузкой в ​​этом случае.

«Непосредственный контекст» здесь является техническим термином, но в основном он означает, что ошибка должна быть «достаточно ранней», чтобы ее можно было обнаружить. Если для поиска ошибки требуется компиляция корпусов функций, это не в «непосредственном контексте».

Это не единственный способ. Лично мне нравится прятать свой код SFINAE за лоском респектабельности. Ниже я использую диспетчеризацию тегов, чтобы «скрыть» сбой где-то еще, вместо того, чтобы помещать его прямо в сигнатуру функции:

template<class T>
struct tag {
  using type=T;
  constexpr tag(tag const&) = default;
  constexpr tag() = default;
  template<class U,
    class=std::enable_if_t<std::is_base_of<T,U>{}>
  >
  constexpr tag(tag<U>) {}
};

struct Base{};
struct Derived:Base{};

template<class T>
void foo( T t, tag<Base> = tag<T>{} ) {
}

здесь мы создаем тип отправки tag, и он позволяет конвертировать в базу. tag позволяет нам использовать типы в качестве значений и использовать над ними более обычные операции C++ (вместо шаблонного метапрограммирования <> повсюду).

Затем мы даем foo второй аргумент типа tag<Base>, а затем создаем его с помощью tag<T>. Это не удается скомпилировать, если T не является производным типом от Base.

живой пример.

Прелесть этого решения в том, что код, из-за которого оно не работает, кажется более интуитивным — tag<Unrelated> не может преобразоваться в tag<Base>. Это, однако, не мешает рассмотрению функции для разрешения перегрузки, что может быть проблемой.

Способ с меньшим количеством котлов:

template<class T>
void foo( T t, Base*=(T*)0 ) {
}

где мы используем тот факт, что указатели могут быть преобразованы, если между ними существует отношение вывода.


В C++11 (и без поддержки constexpr) мы сначала пишем хелпер:

namespace notstd {
  template<bool b, class T=void>
  using enable_if_t=typename std::enable_if<b,T>::type;
}

тогда:

template<class T,
  class=notstd::enable_if_t<std::is_base_of<Foo, T>::value>
>
void foo(T arg) {
}

если вам не нравится помощник, мы получаем это уродливое дополнение:

template<class T,
  class=typename std::enable_if<std::is_base_of<Foo, T>::value>::type
>
void foo(T arg) {
}

второй описанный выше метод C++14 также может быть переведен на C++11.


Вы можете написать псевдоним, который выполняет тест, если хотите:

template<class U>
using base_test=notstd::enable_if_t<std::is_base_of<Base, U>::value>;

template<class T,
  class=base_test<T>
>
void foo(T arg) {
}
person Yakk - Adam Nevraumont    schedule 23.07.2015
comment
+1 Отличный ответ, но, пожалуйста, посмотрите мой последний комментарий к ответу Барри. Есть ли способ избежать написания одного и того же шаблона много раз? Я использую CLion и думаю, что он не полностью понимает С++ 14, поэтому я застрял на С++ 11. - person becko; 23.07.2015
comment
Добавлен краткий псевдоним @becko. Однако обратите внимание, что решение tag довольно краткое. - person Yakk - Adam Nevraumont; 23.07.2015
comment
блеск респектабельности, фыркнул. ваши [отличные, как обычно] ответы всегда сводят меня с ума. - person Barry; 24.07.2015