您的位置:首页 > 产品设计 > UI/UE

条款20 宁以pass-by-reference-to-const替换pass-by-value

2015-03-12 20:27 288 查看
总结:

1、尽量以pass-by-reference-to-const替换pass-by-value。前者更高效且可以避免切断问题。

2、这条规则并不适用于内建类型及STL中的迭代器和函数对象类型。对于它们,pass-by-value通常更合适。

缺省情况下,C++以传值方式将对象传入或传出函数(这是一个从C继承来的特性)。除非你另外指定,否则函数的参数就会以实际参数的副本进行初始化,而函数的调用者会收到函数返回值的一个复件。这个复件由对象的拷贝构造函数生成,这就使得传值成为一个代价不菲的操作。例如,考虑下面这个类继承体系:

class Person {
public:
   Person(); // 为求简化,省略参数
   virtual ~Person();
   ...

private:
   std::string name;
   std::string address;
};

class Student: public Person {
public:
   Student(); // 再次省略参数
   ~Student();
   ...

private:
   std::string schoolName;
   std::string schoolAddress;
};
现在,考虑以下代码,在此我们调用函数validateStudent,它得到一个Student实参(以传值方式),并返回它是否有效:

bool validateStudent(Student s); // 函数以by value方式接受Student
Student plato;
bool platoIsOK = validateStudent(plato); //call the function


很明显,Student的拷贝构造函数被调用,用plato来初始化参数s。同样明显的是,当 validateStudent返回时,s就会被销毁。所以这个函数的参数传递代价是一次Student的拷贝构造函数的调用和一次Student的析构函数的调用。
但这还不是全部。Student对象内部包含两个string对象,Student对象还要从一个 Person对象继承,Person对象内部又包含两个额外的string对象。最终,以传值方式传递一个Student对象的后果就是引起一次Student的拷贝构造函数的调用,一次Person的拷贝构造函数的调用,以及四次string的拷贝构造函数调用。当Student对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个Student的全部代价是六个构造函数和六个析构函数
这是正确和值得的行为。毕竟,你希望全部对象都得到可靠的初始化和销毁。尽管如此,pass by reference-to-const方式会更好:
bool validateStudent(const Student& s);
这样做非常有效:没有任何构造函数和析构函数被调用,因为没有新的对象被构造。修改后参数声明中的const是非常重要的,原先validateStudent以by-value方式接受一个Student参数,所以调用者知道函数绝不会对它们传入的Student做任何改变,validateStudent只能改变它的复件。现在Student以引用方式传递,同时将它声明为const是必要的,否则调用者必然担心validateStudent改变了它们传入的Student
以传引用方式传递参数还可以避免切断问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象行为像一个派生类对象的特化性质被“切断”了,只剩下一个纯粹的基类对象例如,假设你在一组实现一个图形窗口系统的类上工作:
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对象的显示方法。现在,假设你想写一个函数打印出一个窗口的名字,并随后显示这个窗口。以下是错误示范:
void printNameAndDisplay(Window w) //incorrect! 参数可能被切割
{
   std::cout << w.name();
   w.display();
}
考虑当你用一个 WindowWithScrollBars 对象调用这个函数时会发生什么:

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
参数w将被作为一个Window对象构造——它是被传值的,而且使wwsb表现得像一个 WindowWithScrollBars对象的特殊信息都被切断了。在printNameAndDisplay中,全然不顾传递给函数的那个对象的类型,w将始终表现得像一个Window 类的对象(因为其类型是Window)。因此在printNameAndDisplay中调用display将总是调用 Window::display,绝不会是WindowWithScrollBars::display。绕过切断问题的方法就是以passby
reference-to-const方式传递w:

void printNameAndDisplay(const Window& w)
{                      // 参数不会被切割
   std::cout << w.name();
   w.display();
}


现在传进来的窗口是什么类型,w就表现出那种类型。用指针实现引用是非常典型的做法,所以pass by reference实际上通常意味着传递一个指针。由此可以得出结论,如果你有一个内置类型对象(一个int),以传值方式传递它常常比传引用方式更高效;同样的建议也适用于 STL 中的迭代器和函数对象。
一个对象小,并不意味着调用它的拷贝构造函数就是廉价的。很多对象(包括大多数STL容器)内含的东西只比一个指针多一些,但是拷贝这样的对象必须同时拷贝它们指向的每一样东西,那将非常昂贵。即使当小对象有一个廉价的拷贝构造函数,也会存在性能问题。一些编译器对内置类型和用户自定义类型并不一视同仁,即使他们有同样的底层表示。例如,一些编译器拒绝将仅由一个double组成的对象放入一个寄存器中,即使通常它们非常愿意将一个纯粹的double 放入那里。当这种事发生,你以传引用方式传递这样的对象更好一些,因为编译器理所当然会将一个指针(引用的实现)放入寄存器。
小的用户定义类型不一定是传值的上等候选者的另一个原因是:作为用户定义类型,它的大小常常变化。通常情况下,你能合理地假设传值廉价的类型仅有内置类型及STL中的迭代器和函数对象。对其他任何类型,请尽量以pass-by-reference-to-const替换pass-by-value。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐