【问题标题】:Choose between template function and auto type deduction在模板功能和自动类型扣除之间进行选择
【发布时间】:2015-07-18 16:09:51
【问题描述】:

我有一个关于模板函数与函数的自动类型推导的一般性问题。

多年来,我们已经能够编写模板函数:

template <class T> T add(T a,T b){
    return a+b;
}

函数的参数推导有一个使用auto的TS

auto add(auto a,auto b){
    return a+b;
}

我虽然使用自动,但无法获取实际类型,例如使用静态成员,但这很好用:

#include <iostream>

struct foo
{
    static void bar(){std::cout<<"bar"<<std::endl;}
    static int i ;
};
int foo::i{0};
void t(auto f){
    decltype(f)::bar();
    std::cout<<    decltype(f)::i<<std::endl;
}
int main(int argc, char *argv[])
{
    t(foo());
    return 0;
}    

那么有什么理由选择一个而不是另一个?

【问题讨论】:

  • C++14 不允许对函数参数类型使用auto,只能推断返回类型。您所使用的(作为编译器提供的扩展)是最终会在 Concepts Lite 标准化时进入标准的语法。
  • auto 无论如何都对类型使用模板类型推导。
  • @FoggyDay:主要有两个原因。首先是避免冗长。二是避免因同类型的冗余规范而降低可维护性。
  • 请注意,您可以只写f.bar()f.i 来访问静态成员。
  • @T.C. using T = decltype(f); ;)

标签: c++ templates auto c++14 c++17


【解决方案1】:

在您的代码中,auto 有两种不同的用法,一种在参数中,另一种在返回类型中。在参数的情况下,auto 的每次使用都会引入一个唯一的模板类型参数,因此 Jerry 提到它相当于:

// 1
template <typename A, typename B>
auto add(A a, B b) {
    return a + b;
}

在这种情况下,如果您对不同类型的参数有任何约束(必须相同,可以是一个,但会有其他),那么显式模板语法提供了更好的选择。如果您想在参数上使用 SFINAE(通过额外的模板参数),这是特别的:

// 2
template <typename A, typename B, 
          typename _1 = typename A::iterator,  // A has nested iterator type
          typename _2 = typename B::iterator>  // So does B
auto f(A a, B b);

请注意,我有意避免在返回类型上使用 SFINAE,因为它会以某种方式干扰您对 auto 的其他使用,但这可能是另一种选择:

// 3
auto f(auto a, auto b) 
  ->     typename enable_if<has_nested_iterator<decltype(a)>::value
                         && has_nested_iterator<decltype(b)>::value, 
                           [return type] >::type;

但是正如您所看到的,它变得更加复杂,因为您需要使用尾随返回类型并通过decltype 获取值的类型。

在您的示例中,auto 的第二个用法与这个完全不同,它在返回类型中具有 推导的返回类型。推导的返回类型是 C++11 中已经为 lambda 提供的功能,但已推广到所有函数模板。该功能允许编译器通过检查主体内不同的return 语句来查找函数返回的类型。这样做的好处是,如果您的模板只有一个返回表达式,您可以避免输入该表达式两次,一次用于返回类型,另一次用于实际代码:

// 4
auto add(auto a, auto b) -> decltype(a + b) {  // 'a + b' here
   return a + b;                               // 'a + b' also here
}

缺点是编译器需要检查函数体来确定将返回的类型,这必然是后期类型替换。因此,具有推导类型的函数的 return 语句不能在 SFINAE 表达式中使用,这可能会使函数用户的生活变得复杂:

// 5
auto doAdd(auto a, auto b) 
  -> typename enable_if<is_integral<decltype(add(a,b))>>::type
{
   return add(a,b);
}

SFINAE 将不会从重载解决方案集中删除上述 doAdd 重载,如果您将其称为 doAdd(1, 1.),则会导致硬错误。

【讨论】:

  • “在这种情况下,如果您对不同的类型参数有任何约束 [...],那么显式模板语法提供了更好的选择。”什么?鉴于 auto 参数应该通过概念 TS 引入,难道不是使用带有概念的缩写模板表示法的替代方法吗?
  • @dyp:在答案中,我指的是古老的 template&lt;...&gt;... 语法,但是由于您将概念 TS 带到讨论中,因此 TS 将多种不同的语法与不同级别的混合灵活性和等效性。您提到的缩写模板不能用于答案中的示例2和3,因为auto f(HasIterator a, HasIterator b)强制ab的类型相同,而答案中的代码要求每种类型独立满足这个概念。我提出了这个问题,并提出了模板介绍的修改版本 [...]
  • ... 语法,这将允许auto f(HasIterator{T} a, HasIterator{U} b),即让用户为引入的模板参数提供一个名称,相同的名称代表相同的类型,不同的名称代表一个新的类型。安德鲁回复我说他们过去曾考虑过这一点并拒绝了,但没有完全解释原因是什么。在与其他委员会成员的对话中,我被告知 Bjarne 希望概念看起来像类型,因为从概念上讲它只是一个占位符。 [...]
  • ... 在更简单的情况下,这确实很棒,但这意味着缩写模板表示法不允许用户控制哪些参数是相同或不同类型,并强制使用这种情况下更重的语法(例如,zip 算法从TU 的两个序列中生成pair&lt;T,U&gt; 序列)。就我个人而言,在将其纳入标准之前,我会删除语法的一些替代方案,并且我推动我的公司将其作为 cmets 的一部分提供给 TS。
  • 其他我不喜欢的东西:void f(auto a, auto b) 可能不同类型的参数,void f(concept a, concept b) 相同类型的参数——不一致;需求的不同位置导致不同的不兼容声明;非模板可以受到约束,然后受到更多约束:void f(int); void f(int) requires true;。我不明白为什么在非模板代码中具有约束函数[有一个:在模板内内联定义的朋友,该模板存在或不存在取决于模板参数类型的特征,但可以单独列出]。
【解决方案2】:

这个特定代码的明显原因是它们根本没有相同的语义。

特别是,如果您传递不同类型的参数,使用auto 的版本将为每个独立推导出一个类型,然后根据这些推导出一个类型的结果。

相反,对于模板版本,您只指定了一种类型,因此参数必须是相同类型(并且结果将相同)。

因此,要使代码更接近等效,您确实需要将模板编写得更像:

template <class T, class U> 
auto add(T a, U b) -> decltype(a+b) {
    return a+b;
}

这显然也是可能的,但更支持使用auto 的论点。

【讨论】:

  • 您的代码有多种方式不等同于自动版本(decay、sfinae)。
  • “更像”旨在涵盖仍然存在一些差异的观点,但更明确地说明它仍然存在这一事实可能并没有什么坏处并不完全相同。我进行了编辑以反映这一事实。
  • 你为什么不使用template &lt;class T, class U&gt; auto add(T a, U b) {return a+b;} ?它是 C++14。
  • @Columbo:因为重点是要展示您如何不使用使用auto
  • @Columbo 如果a+b 返回一个引用,-&gt;decltype 版本的add 返回一个引用,而auto 版本返回一个值类型(并衰减)。这可能可以通过decltype(auto) add(auto a, auto b){return a+b;} 版本解决(但我对自动模板函数的 C++1z 提案还没有完全了解)。
【解决方案3】:

经过思考,我看到使用模板函数的唯一原因是强制多个参数具有相同的类型。使用 auto 时,每种类型的扣除都是相互独立的。

如果您希望第一个和第三个参数具有相同的类型和第二个参数的泛型类型,您甚至可以混合使用模板和自动。

template <class T> void barb(T a,auto b,T c){}
int main(int argc, char *argv[])
{
    barb(5," ",5); //ok 
    barb(5," ",5.6); //fails
    return 0;
} 

正如 Oktalist 在评论中所写,您可以使用 using 语句来获取参数的类型 auto。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-19
    • 2012-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多