完美转发
根据语言规则,移动语义无法传递。移动语义不自动传递的原因在于确保原对象(被移动的对象)的状态合理。如果移动操作会自动传播到所有相关成员或其他关联的对象,那么可能会在无意间将某个对象移动多次,导致其内部值或资源丢失。因此,每次移动操作都需要明确地调用移动构造函数或移动赋值运算符,并且在完成移动后,原对象应处于一种有效的状态,通常是“空”或者“不可用”。
通常,对于参数的转发,需要实现三种情况:
#include <iostream>
class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}
void f(Base& a) {g(a);} // 调用 g(Base&)
void f(const Base& a) {g(a);} // 调用 g(const Base&)
// void f(Base&& a) {g(a);} // 右值引用不自动传递,所以在f(Base{})时,创建的临时对象只会调用g(Base&)
void f(Base&& a) {
// 虽然传进来的是右值,但是变量a的值类别是非常量左值
// 所以如果不显式调用std::move(), 则会调用g(Base&)
g(std::move(a));
}
int main() {
Base a;
const Base const_a;
f(a); // f(Base&) -> g(Base&)
f(const_a); // f(const Base&) -> g(const Base&)
f(Base{}); // f(Base&&) -> g(Base&&)
f(std::move(a)); // f(Base&&) -> g(Base&&)
}
那么对于模板函数,如何合并这三种操作呢?简单尝试一下:
#include <iostream>
class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}
template<typename T>
void f(T& a) {
g(a);
}
int main() {
Base a;
const Base const_a;
f<Base>(a); // f(Base&) -> g(Base&)
f<const Base>(const_a); // f(const Base&) -> g(const Base&)
// f<Base&&>(Base{}); // f(Base&&) -> g(Base&&)
// f<Base&&>(std::move(a)); // f(Base&&) -> g(Base&&)
}
可以发现,对于Base& 和 const Base&, 函数模板f完成了转发,但是对于右值版本,f无法匹配。或者,可以将T& 修改成T,再试试:
class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}
// 对于没有引用的版本,因为无法显示调用 std::move,在右值传递时候调用的是g(Base&)
template<typename T>
void f(T a) {
g(a);
}
int main() {
Base a;
const Base const_a;
f<Base>(a); // f(Base&) -> g(Base&)
f<const Base>(const_a); // f(const Base&) -> g(const Base&)
f<Base&&>(Base{}); // f(Base&) -> g(Base&)
f<Base&&>(std::move(a)); // f(Base&) -> g(Base&)
}
可以发现,现在f<Base&&>(Base{});至少能编译通过了,但是由于函数模板f没有显示调用std::move(), 所以移动语义无法传递,最终只能调用g(Base&)。
C++11 引入了std::forward,使得参数可以进行完美转发:
#include <iostream>
class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}
// T&&:万能引用
template<typename T>
void f(T&& a) {
g(std::forward<T>(a)); // 完美转发
}
int main() {
Base a;
const Base const_a;
f(a); // g(Base&)
f(const_a); // g(const Base&)
f(Base{}); // g(Base&&)
f(std::move(a)); // g(Base&&)
}
使用 enable_if<> 禁用模板
C++11开始,提供了辅助模板std::enable_if<> ,以在特定的编译时条件下忽略函数模板:
#include <iostream>
// typename std::enable_if<(sizeof(T) > 4)>::type 类型作为返回值
// SFINAE 的模板特性:替换失败不为过
// 若表达式结果为 true,其类型成员类型将产生一个类型
// 若没有传递第二个模板参数,则该类型为 void
// 否则,该类型就是第二个模板参数类型。
// 若表达式结果为 false,则没有定义成员类型。
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
std::cout<<sizeof(T)<<std::endl;
}
int main() {
// foo<int>(); // 编译错误,没有与其匹配的foo函数模板
foo<double>(); // 编译成功 8 字节
// foo<char>(); // 编译错误,没有与其匹配的foo函数模板
foo<std::string>(); // 编译成功 32 字节
return 0;
}
#include <iostream>
// 传入参数的版本, 如果条件为true,则返回类型为T
template<typename T>
typename std::enable_if<(sizeof(T) > 4), T>::type
foo() {
return T{};
}
int main() {
// foo<int>(); // 编译错误,没有与其匹配的foo函数模板
auto a = foo<double>(); // 编译成功 8 字节
std::cout<<a<<sizeof(a)<<std::endl;
// foo<char>(); // 编译错误,没有与其匹配的foo函数模板
auto b = foo<std::string>(); // 编译成功 32 字节
std::cout<<sizeof(b)<<std::endl;
return 0;
}
如果是C++17版本,std::enable_if<>::type 可以简写成 std::enable_if_t<>
上述写法并不直观,所以通常可以写成带默认值的函数模板参数:
#include <iostream>
// 传入参数的版本, 如果条件为true,则返回类型为T
// 如果bool值为true,则等价为:template<typename T, typename = void>
template<typename T,
typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
std::cout<<sizeof(T)<<std::endl;
}
int main() {
// foo<int>(); // 编译错误,没有与其匹配的foo函数模板
foo<double>(); // 编译成功 8 字节
// foo<char>(); // 编译错误,没有与其匹配的foo函数模板
foo<std::string>(); // 编译成功 32 字节
return 0;
}
上述写法可能还是不太直观,那么还可以使用using来设置别名,使得需求更明确:
#include <iostream>
// 重命名一个带条件的模板类型
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T, typename = EnableIfSizeGreater4<T>>
void foo() {
std::cout<<sizeof(T)<<std::endl;
}
int main() {
// foo<int>(); // 编译错误,没有与其匹配的foo函数模板
foo<double>(); // 编译成功 8 字节
// foo<char>(); // 编译错误,没有与其匹配的foo函数模板
foo<std::string>(); // 编译成功 32 字节
return 0;
}
同时,新标准还引入了一个条件判断 std::is_convertible<FROM,TO>, 返回一个bool类型,表示类型FROM 是否可以强制转换为类型TO,结合使用样例为:
#include <iostream>
#include <type_traits>
// 重命名一个带条件的模板类型
template<typename T>
using EnableDouble = std::enable_if_t<(std::is_convertible<T, double>::value)>;
template<typename T, typename = EnableDouble<T>>
void foo() {
std::cout<<"This type can be converted to double."<<std::endl;
}
int main() {
foo<int>(); // int 可以隐式可转换成double
foo<double>(); // double本身可以使用该模板
foo<float>(); // 同理,float可以隐式可转换成double
// foo<std::string>(); // std::string 不能隐式可转换成double,所以不会生成匹配的模板函数
return 0;
}
当然,C++20 提供了concept, 来进一步简化该语法:
#include <iostream>
#include <type_traits>
// 重命名一个带条件的模板类型
template<typename T>
concept EnableDouble = std::is_convertible<T, double>::value;
template<EnableDouble T>
void foo() {
std::cout<<"This type can be converted to double."<<std::endl;
}
int main() {
foo<int>(); // int 可以隐式可转换成double
foo<double>(); // double本身可以使用该模板
foo<float>(); // 同理,float可以隐式可转换成double
// foo<std::string>(); // std::string 不能隐式可转换成double,所以不会生成匹配的模板函数
return 0;
}
引入concept,可以大大增加代码的可读性,语义上也更贴近人的理解。
总结
• 模板中,通过将参数声明为转发引用 (声明为模板参数名称后跟 && 形成的类型) 并在转发调 用中使用 std::forward<>(),就可以“完美”地转发参数了。
• 使用完美转发成员函数模板时,可能会比预定义用于复制或移动对象的特殊成员函数更匹配。
• 使用 std::enable_if<>,可以在编译时条件为 false 时禁用函数模板 (当条件确定,将忽略模板)。
• 概念允许对函数模板需求使用更直观的语法。