Fork me on GitHub

读书笔记 effctive c++ Item 20 优先使用按const-引用传递(by-reference-to-const)而不是按值传递(by value)

1. 按值传递参数会有效率问题

默认情况下,C++向函数传入或者从函数传出对象都是按值传递(pass by value)(从C继承过来的典型特性)。除非你指定其他方式,函数参数会用实际参数值的拷贝进行初始化,函数调用者会获得函数返回值的一份拷贝。这些拷贝由对象的拷贝构造函数生成。这使得按值传递(pass-by-value)变成一项昂贵的操作。举个例子,考虑下面的类继承体系(Item 7):

 1 class Person {
 2 
 3 public:
 4 
 5 Person(); // parameters omitted for simplicity
 6 
 7 virtual ~Person(); // see Item 7 for why this is virtual
 8 
 9 ...
10 
11 private:
12 
13 std::string name;
14 
15 std::string address;
16 
17 };
18 
19 class Student: public Person {
20 
21 public:
22 
23 Student(); // parameters again omitted
24 
25 virtual ~Student();
26 
27 ...
28 
29 private:
30 
31 std::string schoolName;
32 
33 std::string schoolAddress;
34 
35 };

 

现在考虑下面的代码,在这里我们调用了一个函数,validateStudent,这个函数有一个Student参数(按值),返回值表示验证是否通过:

1 bool validateStudent(Student s); // function taking a Student
2 
3 // by value
4 
5 Student plato; // Plato studied under Socrates
6 
7 bool platoIsOK = validateStudent(plato); // call the function

 

当函数被调用时会发生什么?

很清楚,Student拷贝构造函数会被调用,用plato来初始化参数s。同样很清楚的是,当validateStudent函数返回后s会被销毁。所以这个函数参数传递的开销是分别调用了构造函数和析构函数。

但这不是所有的开销。一个Student对象中有两个string对象,所以每次你构建一个Student对象的时候你必须构造两个string对象。Student对象继承自Person对象,所以每次你构建一个Student对象你必须构造一个Person对象。一个Person对象中有两个额外的string对象,所以每个Person构造函数同样需要对两个额外的string进行构造。最后结果是按值传递一个Student对象导致对Student拷贝构造函数的一次调用,对Person拷贝构造函数的一次调用,对stirng拷贝构造函数的四次调用。当Student对象的拷贝被释放时,每个构造函数对应的析构函数要被调用,所以按值传递一个Student对象的总开销是6次构造和6次析构!!

2. 按const引用传递会更高效

这是正确的并且令人满意的行为。毕竟,你需要的是所有对象被可靠的初始化和销毁。并且,如果有一种方法能够绕过这些构造函数和析构函数就再好不过了。这种方法是存在的,就是:按const引用进行传递(pass by reference-to-const

1 bool validateStudent(const Student& s);

 

这种用法更具效率:没有构造函数或者析构函数被调用,因为没有新的对象被创建。在修订后版本的参数声明中,const是很重要的。validataStudent的原始版本有一个按值传递的Studetn参数,调用者会知道对被传递进去的Student参数的任何可能的修改都会被屏蔽掉;validateStudent只是在修改它的一份拷贝。现在Student被按照引用进行传递,将其声明为const同样是必须的,否则调用者就会为传递进去的参数是否被修改而担心。

3. 按const引用传递能避免切片问题

按引用传递参数同样避免了切片(slicing)问题。当一个派生类对象被当作一个基类对象被传递时(按值传递),基类的拷贝构造函数会被调用,“使对象的行为看起来像派生类对象“这个特定的特性被“切掉”了。留给你的只剩下一个基类对象,因为是一个基类的构造函数创建了它。这是你永远不希望看到的。举个例子,假设你正在一些类上进行工作,这些类实现了图形化窗口系统:

 1 class Window {
 2 
 3 public:
 4 
 5 ...
 6 
 7 std::string name() const; // return name of window
 8 
 9 virtual void display() const; // draw window and contents
10 
11 };
12 
13 class WindowWithScrollBars: public Window {
14 
15 public:
16 
17 ...
18 
19 virtual void display() const;
20 
21 };

 

所有的窗口对象都有一个名字,你可以通过name函数来获取它,并且所有的窗口都能被显示出来,你可以通过触发display函数来实现。Display函数为虚函数的事实告诉你基类Windows对象的显示方式同WindowWithScrollBars对象的显示方式是不同的(Item 34和Item 36)。

现在假设你实现了一个函数,先打印窗口的名字然后让窗口显示出来。下面是实现这样一个函数的错误的方式:

1 void printNameAndDisplay(Window w) // incorrect! parameter
2 
3 { // may be sliced!
4 
5 std::cout << w.name();
6 
7 w.display();
8 
9 }

 

考虑当你使用一个WindowWithScrollBars对象作为参数调用这个函数会发生什么:

1 WindowWithScrollBars wwsb;
2 
3 printNameAndDisplay(wwsb);

 

 参数w将会被构造,它是按值传递的,所以w作为一个Window对象,所有让wwsb看起来像一个WIndowWithScrollBars对象的特定信息都会被切除。在printNameAndDispay内部,w的行为总是会像Window对象一样(因为他是一个Window类的对象),而不管传入函数的参数类型是什么。特别的,在printNameAndDisplay内部对display的调用总是会调用Window::display,永远不会调用WindowWithScrollBars::display。

解决切片问题的方法是将w按const引用传递进去(by reference-to-const):

1 void printNameAndDisplay(const Window& w) // fine, parameter won’t
2 
3 { // be sliced
4 
5 std::cout << w.name();
6 
7 w.display();
8 
9 }

 

现在w的行为会和传入参数的实际类型一致了。

4. 什么情况下按值传递是合理的

如果你偷看一下C++编译器的底层,你将会发现引用是按照指针来进行实现的,所以按引用传递一些东西就意味着传递一个指针。因此,如果你有一个内建类型的对象(例如int)按值传递比按引用传递效率更高。对于内建类型来说,当你在按值传递和按引用传递之间进行选择时,选择按值传递是合理的。这对于STL中的迭代器和函数对象同样适用,因为按照惯例,它们被设计成按值传递。迭代器和函数对象的设计者有责任留意下面两个问题:高效的拷贝和不用忍受切片问题。(这是一个规则如何被改变的例子,取决于你使用C++的哪一部分 见 Item 1。)

5. 并不是对象小就应该按值传递

内建类型占用了很少的内存,所以一些人得出结论:所有这样的小的类型都是按值传递的候选者,即使它们是用户定义的类型。这个原因是靠不住的。因为一个对象占用内存少并不意味这调用它的拷贝构造函数不昂贵。许多对象——这些对象中的大多数STL容器——仅仅包含一个指针,但是拷贝这些对象会拷贝它们指向的所有东西。这可是非常昂贵的操作。

即使是当小对象的拷贝构造函数的调用开销很小时,也会有性能问题。一些编译器对于内建类型和用户自定义类型有不同的对待方式,即使它们有相同的底层表示(underlying representation)。举个例子,一些编译器拒绝将只含有一个double数值的对象放入缓存中,却很高兴的为一个赤裸裸的double这么做。当这类事情发生的时候,将这些对象按引用传递会更好,因为编译器会将指针(引用的实现)放入缓存中。

另外一个小的用户自定义类型不是按值传递的好的候选者的原因是,作为用户自定义类型,它们的大小会发生变化。一个类型现在可能很小但是在将来的发布中可能会变的更大,因为它的内部实现可能发生变化。当你切换到一个不同的C++实现时事情也有可能发生变化。举个例子,标准库的string类型的一些实现比其他实现大6倍。

一般情况下,你能够对“按值传递是不昂贵的”进行合理假设的唯一类型就是内建类型和STL迭代器以及函数对象。对于其它的任何类型,遵循这个条款的建议,优先使用按const引用传递而不是按值传递。

6. 总结

  • 优先使用按const-引用传递而不是按值传递。它更具效率并且能够避免切片问题。
  • 这个规则不适用于内建类型,STL迭代器和函数对象类型。对于它们来说,按值传递通常是合适的。
posted @ 2017-02-25 07:53  HarlanC  阅读(931)  评论(1编辑  收藏  举报