传参的类型

完美转发#

我们知道,对于对象的传递可以是普通的引用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]类型。实际上,没有编辑器的检查或者没有足够小心,很难怀疑这是错误的写法,甚至它很符合一贯的用法。

作者:cwtxx

出处:https://www.cnblogs.com/cwtxx/p/18718191

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   cwtxx  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示