仅匹配某些类型的模板函数?

Template function that matches only certain types?

我要定义函数模板:

template<typename T>
void foo(T arg)

但我希望 T 只匹配某些类型。具体来说,T 应该从某个基础 class 派生(可能通过多重继承)。否则此模板不应包含在重载集中。

我该怎么做?

将 SFINAE 与 std::is_base_of 一起使用:

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

如果 T 继承自 Foo,则仅在重载集中包含 foo。请注意,这也包括模棱两可和不可访问的碱基。如果你想要一个只允许 Ts 从 Foo 继承 publiclyunambiguously 的解决方案,那么你可以而是使用 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>::valuetrueenable_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。您可能认为这是一个编译错误,但是 ssubstitution failure is not an e错误 (sfinae)。这只是模板推导失败。因此,在这种情况下,效果是 foo<T> 只是从可行的重载候选集中删除,与任何其他模板推导失败没有区别。

基于 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");
  // ...
}

在精简概念的 C++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 时应用该概念。我不知道怎么做的是模板概念——从模板类型参数生成的概念。


在C++14中,有两种方法。一、普通SFINAE:

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

这里我们创建了一个模板,从它的参数中推导出类型 T。然后它会尝试从第一个参数中推断出它的 second 类型参数。

第二个类型参数没有名称(因此 class=),因为我们仅将其用于 SFINAE 测试。

测试是enable_if_t< condition >。如果 condition 为真,enable_if_t< condition > 生成类型 void。如果 condition 为假,则在 "the immediate context" 中失败,生成替换失败。

SFINAE 是 "Substitution failure is not an error"——如果你的类型 T 在函数模板签名的 "immediate context" 中生成失败,这不会生成编译时错误,但是相反导致函数模板在这种情况下不被视为有效重载。

"Immediate context" 在这里是一个技术术语,但基本上它意味着错误必须是 "early enough" 才能被捕获。如果它需要编译函数体来查找错误,那不在 "the immediate context".

现在,这不是唯一的方法。我个人喜欢把我的 SFINAE 代码藏在体面的光环后面。下面,我使用标签分派 "hide" 其他地方的失败,而不是将其放在函数签名的最前面:

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 dispatch 类型,它允许转换为 base。 tag 让我们将类型 作为值 ,并对它们使用更正常的 C++ 操作(而不是到处都是类似模板的元编程 <>)。

然后我们给 foo 类型 tag<Base> 的第二个参数,然后用 tag<T> 构造它。如果 T 不是 Base 的派生类型,则编译失败。

live example.

此解决方案的好处在于,使它不起作用的代码看起来更直观——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) {
}