Effective C++ 20:传递常量引用比传值更好
Item 20: Prefer pass-by-reference-to-const to pass-by-value
C++函数的参数和返回值默认采用传值的方式,这一特性是继承自 C 语言的。如果不特殊指定, 函数参数将会初始化为实参的拷贝,调用者得到的也是返回值的一个副本。 这些拷贝是通过调用对象的拷贝构造函数完成的,正是这一方法的调用使得拷贝的代价可能会很高。
通常来讲,传递常量引用比传值更好,同时避免了截断问题。但是内置类型和 STL 迭代器,还是传值更加合适。
来个例子
一个典型的类的层级可能是这样的:
class Person {
string name, address;
};
class Student: public Person {
string schoolName, schoolAddress;
};
假如有这样一处函数调用:
bool validateStudent(Student s); // function taking a Student by value
Student plato; // Plato studied under Socrates
bool platoIsOK = validateStudent(plato); // call the function
在调用 validateStudent()
时进行了 6 个函数调用:
Person
的拷贝构造函数,为什么Student
的拷贝构造一定要调用Person
的拷贝构造请参见:Item 12:完整地拷贝对象Student
的拷贝构造函数name
,address
,schoolName
,schoolAddress
的拷贝构造函数
解决办法便是传递常量引用:
bool validateStudent(const Student& s);
首先以引用的方式传递,不会构造新的对象,避免了上述例子中 6 个构造函数的调用。
同时 const
也是必须的:传值的方式保证了该函数调用不会改变原来的 Student
,
而传递引用后为了达到同样的效果,需要使用 const
声明来声明这一点,让编译器去进行检查!
截断问题
将传值改为传引用还可以有效地避免 截断问题:由于类型限制,子类对象被传递时只有父类部分被传入函数。
比如一个 Window
父类派生了子类 WindowWithScrollBars
:
class Window {
public:
...
std::string name() const; // return name of window
virtual void display() const; // draw window and contents
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};
有一个访问 Window
接口的函数,通过传值的方式来获取 Window
的实例:
// incorrect! parameter may be sliced!
void printNameAndDisplay(Window w){
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
当调用 printNameAndDisplay
时参数类型从 WindowWithScrollBars
被隐式转换为 Window
。
该转换过程通过调用 Window
的拷贝构造函数来进行。
导致的结果便是函数中的 w
事实上是一个 Window
对象,
并不会调用多态子类 WindowWithScrollBars
的 display()
。
// fine, parameter won't be sliced
void printNameAndDisplay(const Window& w){
std::cout << w.name();
w.display();
}
这就很好嘛,如果你曾深入过编译器你会发现引用是通过指针来实现的。
特殊情况
一般情况下相比于传递值,传递常量引用是更好的选择。但也有例外情况,比如 内置类型 和 STL 迭代器和函数对象。
内置类型传值更好是因为它们小,而一个引用通常需要 32 位或者 64 位的空间。可能你会认为小的对象也应当首选传值, 但 对象小并不意味着拷贝构造的代价不高!比如 STL 容器通常很小,只包含一些动态内存的指针。然而它的拷贝构造函数中, 必然会分配并拷贝那些动态内存的部分。
即使拷贝构造函数代价很小,传值的方式仍然有性能问题。有些编译器会区别对待内置类型和用户定义类型,
即使它们有相同的底层表示。比如有些编译器虽然会把 double
放入寄存器,但是拒绝将只含一个 double
的对象放入寄存器。
一个只含
double
的对象大小为 8,它和一个double
具有相同的大小和底层表示。关于对象大小的计算,请参考:Item 7:将多态基类的析构函数声明为虚函数
从面向对象设计方面来讲,即使对象现在很小,但它作为用户定义类型是有可能变大的(如果你更改了内部实现)。 从长远来讲的性能考虑,也应当采取传引用的方式来设计使用它的函数。
STL 迭代器和函数对象也应当被传值,这是因为它们在 STL 中确实是被这样设计的,同时它们的拷贝构造函数代价并不高。
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2015/08/13/effective-cpp-20.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。