最近学习C++,书上红字标出的话不太理解,求大神解释!
effective modern C++ 书的部分原文如下:
在另一方面,一个universal引用可能(译注:只是可能不是一定)被绑定到一个有资格被move的对象上去。universal引用只在它由右值初始化的时候需要被转换成一个右值。Item 23解释了这就是std::forward具体做的事情:
程序代码:
class Widget { public: template<typename T> void setName(T&& newName) // newName是一个 { name = std::forward<T>(newName); } // universal引用 ... };
总之,因为右值引用总是被绑定到右值,所以当它们被转发给别的函数的时候,应该被无条件地转换成右值(通过std::move),而universal引用由于只是不定时地被绑定到右值,所以当转发它们时,它们应该被有条件地转换成右值(通过std::forward)。
Item 23解释了对右值引用使用std::forward能让它显示出正确的行为,但是源代码会因此变得冗长、易错、不符合习惯的,所以你应该避免对右值引用使用std::forward。对universal引用使用std::move是更加糟糕的想法,因为这样会对左值(比如,局部变量)产生非预期的修改:
程序代码:
class Widget { public: template<typename T> void setName(T&& newName) // universal引用 { name = std::move(newName); } // 能通过编译,但是 ... // 这代码太糟糕了 private: std::string name; std::shared_ptr<SomeDataStructure> p; }; std::string getWidgetName(); // 工厂函数 Widget w; auto n = getWidgetName(); // n是局部变量 w.setName(n); // 把n move到w中去! ... // n的值现在是未知的
这里,局部变量n被传给w.setName,调用者完全可以假设这是一个对n只读的操作。但是因为stdName在内部会用了std::move,然后无条件地将他的引用参数转换成了右值,所以n的值将被move到w.name中去,最后在setNamen调用完成之后,n将成为一个未知的值。这样的行为会让调用者很沮丧,甚至会气得砸键盘!
你可能指出stdName不应该声明它的参数为universal引用。虽然这样的引用不能是const的(看Item 24,译注:加const就成右值引用了),但是steName确实不应该修改它的参数。你还可能指出如果setName使用const 左值和右值进行重载,整个问题将被避免。像是这样:
程序代码:
class Widget { public: void setName(const std::string& newName) // 从const左值来set { name = newName; } void setName(std::string&& newName) // 从右值来set { name = std::move(newName); } ... };
在这种情况下,确实能工作,但是这种方法是有缺点的。首先,它增加了源代码里要编写以及维护的代码量(使用两个函数代替一个简单的模板)。其次,它更加低效。举个例子,考虑这个setName的使用:
w.setName("Adela Novak");
使用universal引用版本的setName,在“Adela Novak”字符串被传给setName时,它会被转发给处于w对象中的一个std::string(就是w.name)的operator=(译注:const char版本的operator=)函数。因此,w的name数据成员将是用字符串直接赋值的;没有出现一个临时的std::string对象。然而,使用重载版本的setName,为了让setName的参数能绑定上去,一个临时的std::string对象将被创建,然后这个临时的std::string对象将被移动到w的数据成员中去。因此这个setName的调用需要执行一次std::string的构造函数(为了创建临时对象),一个std::string的move operator=(为了move newName到w.name中去),以及一个std::string的析构函数(为了销毁临时对象)。对于const char 指针来说,比起只调用std::string的operator=,上面这些函数就是多花的代价。额外的代价有可能随着实现的不同而产生变化,并且代价是否值得考虑也将随着应用和函数库的不同而产生变化。不管怎么说,事实就是,在一些情况下,使用一对重载了左值和右值的函数来替换带universal引用参数的函数模板有可能增加运行期的代价。如果我们推广这个例子,使得Widget的数据成员可以是任意类型的(不仅仅是熟知的std::string),性能的落差将更大,因为不是所有类型的move操作都和std::string一样便宜的。
我的疑问:
在使用重载版本的setName函数时,w.setName("Adela Novak"); 这句代码难道不是调用的void setName(const std::string& newName)这个函数吗?根据字符串字面值“Adela Novak”创建的临时string对象绑定到函数参数newName,然后执行name = newName;这句代码,其中的newName因为是函数参数,肯定是一个左值,把左值赋值给w的成员name,难道不是应该调用复制赋值运算符吗?为什么书上说是调用移动到w的数据成员中去(move operator=) ?求大神解惑。
[此贴子已经被作者于2019-10-5 10:30编辑过]