通用引用T&&与右值引用std::string&&的性能对比探究
通用引用 vs 右值引用的性能差异解析(基于《Effective Modern C++》Item25)
咱们来仔细拆解这个经典场景,先把代码摆出来:
#include <string> class WidgetURef { public: template<typename T> void setName(T&& newName) { name = std::move(newName); } private: std::string name; }; class WidgetRRef { public: void setName(std::string&& newName) { name = std::move(newName); } private: std::string name; }; int main() { WidgetURef w_uref; w_uref.setName("Adela Novak"); WidgetRRef w_rref; w_rref.setName("Adela Novak"); }
注:通用引用场景本应使用
std::forward,这里为了突出核心问题做了简化。
逐个解答你的问题
1. 两种实现的性能差异到底在哪?
最核心的区别就是临时std::string对象的创建与否:
- 对于
WidgetRRef的setName,参数是固定的std::string&&——但你传入的是字符串字面量"Adela Novak",它的类型是const char[11],根本不是std::string。编译器没办法直接把字面量绑定到std::string&&上,必须先创建一个临时的std::string对象,用这个字面量初始化它,再把这个临时对象(右值)传给setName。这一步会带来std::string构造(内存分配、字符复制)和析构(内存释放)的额外开销。 - 而
WidgetURef的setName是通用引用模板,编译器会做类型推导:把T推导成const char[11],所以newName的类型是const char(&&)[11](数组类型的右值引用)。当执行name = std::move(newName)时,std::move(newName)会退化成const char*,直接调用std::string的赋值运算符(接受const char*的版本),完全不需要创建临时std::string,直接完成字符串的复制赋值,省掉了临时对象的开销。
2. WidgetURef除了类型推导,其他逻辑和WidgetRRef完全一致吗?
当然不一致。除了类型推导的区别,两者的参数类型、赋值时调用的运算符都不一样:
WidgetRRef的参数是std::string&&,赋值时调用的是std::string的移动赋值运算符,但前提是已经有了一个临时的std::string对象。WidgetURef的参数是推导后的数组右值引用,赋值时调用的是std::string接受const char*的普通赋值运算符,直接从C风格字符串完成赋值,根本没有移动操作的前提(因为连临时std::string都不存在)。
3. 两者参数都是&&,是不是都不会产生临时对象?这个推理对吗?
这个推理完全错误。虽然都是&&,但本质天差地别:
WidgetURef的T&&是通用引用,它的厉害之处在于可以绑定到任何类型的实参——左值、右值、数组、指针都行。这里它直接绑定到了字符串字面量本身,不需要任何类型转换,自然不会产生临时对象。WidgetRRef的std::string&&是普通右值引用,只能绑定到std::string类型的右值。而字符串字面量不是std::string,所以必须先创建临时std::string来适配类型,这就凭空多了一个临时对象。
为什么书中说通用引用版本不会产生临时对象?
核心原因就是通用引用的精准类型推导:它能完美匹配实参的原始类型(这里是字符串字面量的数组类型),不需要任何类型转换就能直接传递给赋值运算符。而右值引用版本因为参数类型固定死了是std::string&&,实参类型不匹配,必须通过创建临时std::string来完成类型适配,这就引入了额外的开销。
另外提一句:通用引用的正确用法应该是用std::forward而不是std::move,不过在这个场景下两者效果差不多——但std::forward能根据实参的实际值类别(左值/右值)做转发,更符合通用引用的设计意图,这里只是为了简化才用了std::move。
内容的提问来源于stack exchange,提问作者banach-space
相关产品推荐
相关产品推荐

