传参的类型
完美转发#
我们知道,对于对象的传递可以是普通的引用X&
,常量引用const X&
,或者是右值引用X&&
。
如果不使用模板编程想要将参数转发给其他函数,并区分这三者,就需要重载三个函数来实现。
void g(X&) {
cout << "variable reference\n";
}
void g(const X&) {
cout << "const reference\n";
}
void g(X&&) {
cout << "movable reference\n";
}
void f(X& val) {
g(val);
}
void f(const X& val) {
g(val);
}
void f(X&& val) {
g(std::move(val));
}
int main() {
X x;
const X c;
f(x); // f(X&) => g(X&)
f(c); // f(const X&) => g(const X&)
f(X()); // f(X&&) => g(X&&)
f(std::move(x));// f(X&&) => g(X&&)
return 0;
}
需要说明一点,在转发右值类型的f()
函数中,使用了std::move
,这是因为参数并不传递移动语义的信息,也就是说在f(X&& val)
函数内部,其实val是被看作一个左值的,为了保证类型的无损,所以需要使用std::move
显式指定我们要传递的类型是右值类型。
如果采用模板来写转发函数,就可以少写一些实现,但会遇到一个问题,无法无损地传递参数类型。
template<typename T>
f(T val) {
g(val);
}
上面这个转发函数就无法传递可移动的对象,正如上面说的,f函数内部的val参数是看作左值来使用的。
为了解决这个问题,C++引入了完美转发的特殊规则。
template<typename T>
f(T&& val) {
g(std::forward<T>(val));
}
完美转发std::forward
会根据模板参数T来推导其完整的类型,然后进行无损的转发。
T&& 和 X&&:
模板参数T&&和具体类型X&&是不一样的。
X&&是明确的类型X的右值引用,只能绑定到一个类型X的可移动对象上;
T&&是声明了一个转发引用(万能引用),其遵循C++的引用折叠规则,可以绑定到模板参数T类型的变量对象、常量对象(const修饰的)、或者可移动对象上。
值类别#
C++11以后引入了右值引用来支持可移动对象,值的类别就变得稍微复杂了许多,表达式的值仍可以大体上分为左值和右值两类,但右值还可以进一步细分:
表达式
↙ ↘
广义左值 右值
↙ ↘ ↙ ↘
左值 将亡值 纯右值
左值和纯右值的含义很明确,可以简单的通过是否有对象拥有其所有权来判断。将亡值的含义是指这个变量即将过期,并且它的值可以被重复利用,所以将亡值是左值,此时它是有着明确的内存和对象的所有权的,但是在过期后被再次使用时,它已经释放掉了所有权,变成了右值,例如函数的返回结果就是一个将亡值。如果有其他的变量会重复使用它的话,那么完全有理由不直接释放,而是再次延长它的生命周期,这样可以节省一次构造和析构的开销。这也是编译器对将亡值常做的优化,比如(RVO和NRVO)。
传值还是传引用,这是个问题#
首先,不考虑性能影响的话,传值和传引用对模板来说的最大区别在于,是否允许模板参数的退化。传值是允许参数自动进行退化的,而传引用的情况是不会自动进行退化的,除非在内部显式地使用退化。
template <typename T>
void foo(const T t) {
cout << sizeof(t) << endl;
}
template <typename T>
void foo2(const& T t) {
cout << sizeof(t) << endl;
}
int main() {
char arr[3]{"ab"};
foo(arr);
foo2(arr);
}
为了说明参数的退化,这里有两个模板函数,foo是传值的,foo2是传引用的,它们都是简单的打印一下入参的字节大小,然后将一个三字节的数组作为入参,可以先猜一下它们分别打印出来。
公布结果:
8
3
按值传递的foo函数打印出来的是8,说明数组在传入时,将其处理成了指针,我们打印的是这个指针的大小,在64位操作系统上指针需要占用8字节的大小;而按引用传递的foo2函数打印出来的是3,也就是说,数组被正确地作为入参传入了,我们传入的是引用,实际上计算出来的大小是这个引用指向的对象的大小,也就是最开始定义的数组,占了3个字节。
这并没有好坏之分,只是需要在合适的场景使用合适的方法传递参数(当然,这明显增加了开发者的心智负担)。不过传值似乎更加符合一贯的认知(众所周知,数组是不能作为C的入参的,数组的入参一直会被转换成指针),因此在模棱两可的时候选择按值传递可能更简单点,书中推荐在以下场景中可以使用按引用传递:
- 参数无法拷贝。
- 参数用于返回数据。
- 模板保留原始实参的所有属性,只是转发参数到其他地方。
- 性能能获得明显的提升。
传递引用时,要尤其注意字符串字面量和原始数组的问题,正如刚提到的,传递引用不会将参数退化处理:
template<typename T>
foo(const T& a, const T& b) {
// ...
}
foo("hi", "guys");
这在编写时就会报错,因为"hi"是const char[3]类型,而"guys"是const char[5]类型。实际上,没有编辑器的检查或者没有足够小心,很难怀疑这是错误的写法,甚至它很符合一贯的用法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗