在模板函数和自动类型推导之间选择
Choose between template function and auto type deduction
我有一个关于模板函数与函数的自动类型推导的一般性问题。
多年来,我们已经能够编写模板函数:
template <class T> T add(T a,T b){
return a+b;
}
函数参数推导auto有TS
auto add(auto a,auto b){
return a+b;
}
我虽然使用 auto,但无法获得实际类型,例如使用静态成员,但这工作得很好:
#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;
}
那么有什么理由选择一个而不是另一个吗?
此特定代码的明显原因是它们实际上根本不具有相同的语义。
特别是,如果您传递不同类型的参数,使用 auto
的版本会为每个参数独立推导一个类型,然后根据这些推导结果的类型。
相反,对于模板版本,您只指定了一种类型,因此参数必须是同一类型(结果将是相同的)。
因此,为了使代码更接近等效,您确实需要将模板编写得更像:
template <class T, class U>
auto add(T a, U b) -> decltype(a+b) {
return a+b;
}
这显然也是可能的,但会为支持使用 auto
的论据增添更多力量。
经过思考,我认为使用模板函数的唯一原因是强制多个参数具有相同的类型。而使用 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。
在您的代码中,auto
有两种不同的用途,一种用于参数,另一种用于 return 类型。对于参数,每次使用 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);
请注意,我有意避免在 return 类型上使用 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;
但是正如您所见,它变得更加复杂,因为您需要使用尾随 return 类型并通过 decltype
.
的值获取类型
在你的例子中,auto
的第二次使用,与这个完全不同,是在 return 类型中有一个 推导的 return 类型。推导的 return 类型是 C++11 中已可用于 lambda 的一项功能,但已推广到所有函数模板。该功能使编译器可以通过检查函数体内不同的 return
语句来找到由函数编辑的类型 return。优点是,如果您的模板只有一个 return 表达式,您可以避免键入该表达式两次,一次用于 return ,另一次用于实际代码:
// 4
auto add(auto a, auto b) -> decltype(a + b) { // 'a + b' here
return a + b; // 'a + b' also here
}
缺点是编译器需要检查函数体以确定将 returned 的类型,而这必然是 post 类型替换。因此,来自具有推导类型的函数的 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.)
.[=23,则会导致硬错误=]
我有一个关于模板函数与函数的自动类型推导的一般性问题。
多年来,我们已经能够编写模板函数:
template <class T> T add(T a,T b){
return a+b;
}
函数参数推导auto有TS
auto add(auto a,auto b){
return a+b;
}
我虽然使用 auto,但无法获得实际类型,例如使用静态成员,但这工作得很好:
#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;
}
那么有什么理由选择一个而不是另一个吗?
此特定代码的明显原因是它们实际上根本不具有相同的语义。
特别是,如果您传递不同类型的参数,使用 auto
的版本会为每个参数独立推导一个类型,然后根据这些推导结果的类型。
相反,对于模板版本,您只指定了一种类型,因此参数必须是同一类型(结果将是相同的)。
因此,为了使代码更接近等效,您确实需要将模板编写得更像:
template <class T, class U>
auto add(T a, U b) -> decltype(a+b) {
return a+b;
}
这显然也是可能的,但会为支持使用 auto
的论据增添更多力量。
经过思考,我认为使用模板函数的唯一原因是强制多个参数具有相同的类型。而使用 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。
在您的代码中,auto
有两种不同的用途,一种用于参数,另一种用于 return 类型。对于参数,每次使用 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);
请注意,我有意避免在 return 类型上使用 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;
但是正如您所见,它变得更加复杂,因为您需要使用尾随 return 类型并通过 decltype
.
在你的例子中,auto
的第二次使用,与这个完全不同,是在 return 类型中有一个 推导的 return 类型。推导的 return 类型是 C++11 中已可用于 lambda 的一项功能,但已推广到所有函数模板。该功能使编译器可以通过检查函数体内不同的 return
语句来找到由函数编辑的类型 return。优点是,如果您的模板只有一个 return 表达式,您可以避免键入该表达式两次,一次用于 return ,另一次用于实际代码:
// 4
auto add(auto a, auto b) -> decltype(a + b) { // 'a + b' here
return a + b; // 'a + b' also here
}
缺点是编译器需要检查函数体以确定将 returned 的类型,而这必然是 post 类型替换。因此,来自具有推导类型的函数的 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.)
.[=23,则会导致硬错误=]