条款20:宁以pass-by-reference-to-const替换pass-by-value
本条款的要点:
1、尽量以pass-by-reference-to-const替换pass-by-value。前者更高效且可以避免切割问题。
2、这条规则并不适用于内建类型及STL中的迭代器和函数对象类型。对于它们,pass-by-value通常更合适。
缺省的情况下,C++以by-value方式传递对象至函数,或者获取函数的对象返回值。除非你另外的指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的也是函数返回值的一个副本。这些副本都是有对象的copy构造函数得到的。这可能使得pass-by-value成为昂贵的操作,同时也可能带来对象的切割问题。
效率的考量
对于pass-by-reference的效率的问题看下面class的继承体系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Person { public : Person(); virtual ~Person(); ... private : string name; string address; }; class Student: public Person { public : Student(); ~Student(); ... private : string schoolName; string schoolAddress; }; |
现在考虑下面代码,其中调用函数validateStudent,后者需要一个Student实参(pass-by-value)并返回它是否有效:
1 2 | bool validateStudent(Student s); //声明一个函数,函数以by value方式接受学生 Student plato; //类的对象platobool stuIsOK=validateStudent(plato); |
当上述函数被调用时,发生了什么事?
很明显,Student的拷贝构造函数被调用,用plato来初始化参数s。同样明显的是,当 validateStudent返回时,s就会被销毁。所以这个函数的参数传递代价是一次Student的拷贝构造函数的调用和一次Student的析构函数的调用。
但这还不是全部。Student对象内部包含两个string对象,Student对象还要从一个 Person对象继承,Person对象内部又包含两个额外的string对象。最终,以传值方式传递一个Student对象的后果就是引起一次Student的拷贝构造函数的调用,一次Person的拷贝构造函数的调用,以及四次string的拷贝构造函数调用。当Student对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个Student的全部代价是六个构造函数和六个析构函数!
这是正确和值得的行为。毕竟,你希望全部对象都得到可靠的初始化和销毁。尽管如此,pass by reference-to-const方式会更好:
1 | bool validateStudent( const Student& s); |
这样做非常有效:没有任何构造函数和析构函数被调用,因为没有新的对象被构造。
修改后参数声明中的const是非常重要的,原先validateStudent以by-value方式接受一个Student参数,所以调用者知道函数绝不会对它们传入的Student做任何改变,validateStudent只能改变它的副本。现在Student以引用方式传递,同时将它声明为const是必要的,否则调用者必然担心validateStudent改变了它们传入的Student。
const Student& s表示不能通过reference s修改传进来的Student对象(并不是说这个传进来的Student对象是read-only的)。类似于const int *a;这样的定义。
对象切割问题
以传引用方式传递参数还可以避免切割问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象行为像一个派生类对象的特化性质被“切断”了,只剩下一个纯粹的基类对象例如,假设你在一组实现一个图形窗口系统的类上工作:
1 2 3 4 5 6 7 8 9 10 11 12 | class Window { public : ... std::string name() const ; // 返回窗口名称 virtual void display() const ; // 显示窗口及其内容 }; class WindowWithScrollBars: public Window { public : ... virtual void display() const ; }; |
所有Window对象都有一个名字(name函数),而且所有的窗口都可以显示(display函数)。display为 virtual的事实清楚地告诉你:基类的Window对象的显示方法有可能不同于专门的WindowWithScrollBars对象的显示方法。现在,假设你想写一个函数打印出一个窗口的名字,并随后显示这个窗口。以下是错误示范:
1 2 3 4 5 | void printNameAndDisplay(Window w) //incorrect! 参数可能被切割 { std::cout << w.name(); w.display(); } |
考虑当你用一个 WindowWithScrollBars 对象调用这个函数时会发生什么:
1 2 | WindowWithScrollBars wwsb; printNameAndDisplay(wwsb); |
参数w将被作为一个Window对象构造——它是被传值的,而且使wwsb表现得像一个 WindowWithScrollBars对象的特殊信息都被切割了。在printNameAndDisplay中,全然不顾传递给函数的那个对象的类型,w将始终表现得像一个Window 类的对象(因为其类型是Window)。因此在printNameAndDisplay中调用display将总是调用 Window::display,绝不会是WindowWithScrollBars::display。其实就是派生类对象被强制转换成了基类对象。
绕过切割问题的方法就是以passby reference-to-const方式传递w:
1 2 3 4 5 | void printNameAndDisplay( const Window& w) { // 参数不会被切割 std::cout << w.name(); w.display(); } |
现在传进来的窗口是什么类型,w就表现出那种类型
pass-by-value和pass-by-reference-to-const
小对象该pass-by-value还是pass-by-reference-to-const:
(1)一个对象小,并不意味着调用它的拷贝构造函数就是廉价的。很多对象(包括大多数STL容器)内含的东西只比一个指针多一些,但是拷贝这样的对象必须同时拷贝它们指向的每一样东西,那将非常昂贵。即使当小对象有一个廉价的拷贝构造函数,也会存在性能问题。一些编译器对内置类型和用户自定义类型并不一视同仁,即使他们有同样的底层表示。例如,一些编译器拒绝将仅由一个double组成的对象放入一个寄存器(reg)中,即使通常它们非常愿意将一个纯粹的double 放入那里。当这种事发生,你以传引用方式传递这样的对象更好一些,因为编译器理所当然会将一个指针(引用的实现)放入寄存器。
(2)小的用户定义类型不一定是传值的上等候选者的另一个原因是:作为用户定义类型,它的大小常常变化,因为将来可能会变的很大。
结论是:小对象也尽量pass-by-reference-to-const
用指针实现引用是非常典型的做法,所以pass by reference实际上通常意味着传递一个指针。
由此可以得出结论,如果你有一个内置类型对象(一个int),以传值方式传递它常常比传引用方式更高效;同样的建议也适用于 STL 中的迭代器和函数对象。
因为内置数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
通常情况下,你能合理地假设传值廉价的类型仅有内置类型及STL中的迭代器和函数对象。对其他任何类型,请尽量以pass-by-reference-to-const替换pass-by-value。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 现代计算机视觉入门之:什么是图片特征编码
· .NET 9 new features-C#13新的锁类型和语义
· Sdcb Chats 技术博客:数据库 ID 选型的曲折之路 - 从 Guid 到自增 ID,再到
· 语音处理 开源项目 EchoSharp
· 《HelloGitHub》第 106 期
· Spring AI + Ollama 实现 deepseek-r1 的API服务和调用
· 使用 Dify + LLM 构建精确任务处理应用