【问题标题】:Template function that matches only certain types?仅匹配某些类型的模板函数?
【发布时间】:2015-07-23 19:35:02
【问题描述】:

我要定义一个函数模板:

template<typename T>
void foo(T arg)

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

我该怎么做?

【问题讨论】:

  • @ShafikYaghmour 请注意,它不是重复的。我想限制一个 function 模板,而链接的问题是指一个 class 模板。
  • @ShafikYaghmour 如果这是完全相同的副本并且您确实对另一个有答案,您不会关闭它吗?
  • @Barry 我不认为它是完全重复的......顺便说一句,我提到我不会使用 dup 锤的唯一原因是如果我对提议的 dup 有答案。
  • @ShafikYaghmour 为什么不呢?不想觉得自己在玩系统?

标签: c++ templates typetraits


【解决方案1】:

将 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。请注意,这也包括模棱两可和不可访问的基础。如果您想要一个仅允许从Foo 继承公开明确Ts 的解决方案,那么您可以改用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&lt;Foo, T&gt;::valuetrueenable_if_t 会被实例化为void,就像我们这样写:

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

但是,如果T 没有从Foo 继承,那么类型特征将评估为false,而std::enable_if_t&lt;false&gt; 是替换失败——没有typename enable_if&lt;false&gt;::type。您可能认为这是一个编译错误,但 s替换 failure is not a n e错误(sfinae)。只是模板推演失败。所以效果是foo&lt;T&gt; 在这种情况下被简单地从可行的重载候选集中删除,与任何其他模板推导失败没有什么不同。

【讨论】:

  • 我找不到std::enable_if_t。应该是std::enable_if
  • template&lt;bool b, class T=void&gt;using enable_if_t=typename std::enable_if&lt;b,T&gt;::type;enable_if_t 的单行实现。将其粘贴在namespace notstd 中,当您的编译器更新时,将notstd::enable_if_t 切换为std::enable_if_t。别名非常值得额外的样板,enable_if 的语法没有它是烦人的。
  • 这就是我想要的,谢谢 (+1)。您介意至少在表面上解释一下这是如何工作的吗?
  • @Yakk 另外,我将编写很多函数,如foo,并且只有当T 继承自同一个基类时,所有函数都应该匹配。有没有我可以使用的速记,typedef 之类的,以避免每次都拼写整个template&lt;typename T, ... enable_if_t ...
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    在 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。然后它会尝试从第一个参数推断出它的 第二个 类型参数。

    第二种类型的参数没有名称(因此class=),因为我们只将它用于 SFINAE 测试。

    测试是enable_if_t&lt; condition &gt;。如果condition 为真,enable_if_t&lt; condition &gt; 将生成void 类型。如果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++ 操作(而不是到处使用类似模板的元编程 &lt;&gt;s)。

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

    live example.

    这个解决方案的好处是使它不起作用的代码看起来更直观——tag&lt;Unrelated&gt; 无法转换为tag&lt;Base&gt;。但是,这并不会阻止该函数被考虑用于重载解决方案,这可能是一个问题。

    样板少的一种方法是:

    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) {
    }
    

    【讨论】:

    • +1 很好的答案,但请参阅我对巴里答案的最后评论。是否有避免多次编写相同模板的速记?我正在使用 CLion,我认为它不完全理解 C++14,所以我坚持使用 C++11。
    • @becko 简短别名已添加。但请注意,tag 解决方案非常简短。
    • “体面的光泽”,snort。你的[像往常一样出色]的回答总是让我兴奋。
    猜你喜欢
    • 2010-10-26
    • 1970-01-01
    • 2015-11-22
    • 1970-01-01
    • 1970-01-01
    • 2021-11-12
    相关资源
    最近更新 更多