Effective C++ 条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
Effective C 条款20宁以 pass-by-reference-to-const 替换 pass-by-value核心观点缺省情况下C 以 by value 方式传递对象。但除非对于内置类型、STL 迭代器和函数对象否则 pass-by-reference-to-const 往往比 pass-by-value 更高效并且可以避免对象切割问题。一、pass-by-value 的隐藏代价在 C 中如果不加任何修饰函数参数默认按值传递。这意味着每次调用函数时编译器都会调用参数的拷贝构造函数创建一个副本。1.1 一个简单的例子#includeiostream#includestringclassPerson{private:std::string name_;std::string address_;std::string phone_;public:Person(conststd::stringname,conststd::stringaddr,conststd::stringphone):name_(name),address_(addr),phone_(phone){std::coutPerson 构造\n;}Person(constPersonother):name_(other.name_),address_(other.address_),phone_(other.phone_){std::coutPerson 拷贝构造\n;}~Person(){std::coutPerson 析构\n;}};// ❌ 按值传递每次调用都发生拷贝voidprocessPersonByValue(Person p){std::cout处理中...\n;}// p 在这里析构// ✅ 按 const 引用传递零拷贝voidprocessPersonByRef(constPersonp){std::cout处理中...\n;}intmain(){Personalice(Alice,Beijing,1234567890);std::cout 按值传递 \n;processPersonByValue(alice);std::cout\n 按引用传递 \n;processPersonByRef(alice);return0;}输出Person 构造 按值传递 Person 拷贝构造 处理中... Person 析构 按引用传递 处理中...按值传递时发生了一次拷贝构造和一次析构。如果Person类更复杂包含更多成员、更深的继承层次这个代价会成倍增长。1.2 继承体系中的代价放大classStudent:publicPerson{private:std::string school_;std::vectordoublegrades_;public:Student(conststd::stringname,conststd::stringschool):Person(name,,),school_(school){std::coutStudent 构造\n;}Student(constStudentother):Person(other),school_(other.school_),grades_(other.grades_){std::coutStudent 拷贝构造\n;}};// 按值传递 StudentvoidprocessStudentByValue(Student s){std::cout处理学生...\n;}当按值传递Student时调用Student的拷贝构造函数Student的拷贝构造函数调用Person的拷贝构造函数Person的拷贝构造函数拷贝 3 个std::string然后拷贝school_又一个std::string然后拷贝grades_整个vector涉及堆内存分配总共发生了多少次内存分配远超你的想象二、pass-by-reference-to-const 的优势2.1 性能优势voidprocessByConstRef(constPersonp);这种方式的优势方面pass-by-valuepass-by-reference-to-const拷贝开销调用拷贝构造函数无传递地址通常 4/8 字节析构开销函数返回时析构副本无堆内存分配取决于对象成员无修改安全性函数内可修改副本const 保证不可修改原对象2.2 避免对象切割问题这是 pass-by-value 更严重的隐患——对象切割Object Slicing。#includeiostreamclassWindow{public:virtualvoiddisplay()const{std::cout显示基础窗口\n;}virtual~Window()default;};classWindowWithScrollBar:publicWindow{public:voiddisplay()constoverride{std::cout显示带滚动条的窗口\n;}};// ❌ 按值传递发生对象切割voidshowWindowBad(Window w){// WindowWithScrollBar 被切割成 Windoww.display();// 输出显示基础窗口多态失效}// ✅ 按引用传递保持多态性voidshowWindowGood(constWindoww){w.display();// 正确调用派生类的版本}intmain(){WindowWithScrollBar myWindow;std::cout 按值传递切割\n;showWindowBad(myWindow);// ❌ 输出显示基础窗口std::cout\n 按引用传递多态\n;showWindowGood(myWindow);// ✅ 输出显示带滚动条的窗口return0;}对象切割的本质当WindowWithScrollBar对象按值传递给Window参数时编译器只会拷贝Window子对象的部分。派生类特有的成员如滚动条数据被切掉了。同时vptr也被重置为Window的虚函数表导致多态性完全丧失。⚠️对象切割是静默的灾难代码能编译能运行但行为完全错误。这种问题极难调试。三、例外情况什么时候用 pass-by-value虽然条款标题说宁以 pass-by-reference-to-const 替换 pass-by-value但也有明确的例外3.1 内置类型// ✅ 内置类型按值传递更高效voidprocess(intvalue);// 4 字节拷贝非常快voidprocess(doublevalue);// 8 字节拷贝voidprocess(charvalue);// 1 字节拷贝voidprocess(boolvalue);// 1 字节拷贝// ❌ 按引用传递内置类型反而更慢需要解引用voidprocessBad(constintvalue);// 不必要的间接访问原因内置类型的拷贝就是简单的位复制通常只需一个 CPU 指令。而引用传递需要额外的间接寻址。3.2 STL 迭代器#includevector// ✅ 迭代器按值传递voidprocessIterator(std::vectorint::iterator it);// ✅ 函数对象仿函数按值传递templatetypenameFuncvoidapplyOperation(Func f);// Func 是函数对象原因迭代器和函数对象通常设计为轻量级、可高效拷贝的类型。它们往往就是指针大小或包含少量状态。3.3 小型且拷贝廉价的类型// ✅ 小型类可以按值传递structPoint2D{floatx,y;};voidmovePoint(Point2D p,floatdx,floatdy);// 8 字节拷贝极快structColor{uint8_tr,g,b,a;};voidsetColor(Color c);// 4 字节判断标准如果类型的拷贝构造函数本质上就是memcpy没有堆分配、没有复杂逻辑且大小不超过 2-3 个指针按值传递通常是可接受的。四、现代 C移动语义的影响C11 引入的移动语义改变了游戏规则#includevector#includestringclassBigData{private:std::vectordoubledata_;std::string metadata_;public:BigData()default;// 拷贝构造深拷贝昂贵BigData(constBigDataother):data_(other.data_),metadata_(other.metadata_){}// 移动构造浅拷贝廉价BigData(BigDataother)noexcept:data_(std::move(other.data_)),metadata_(std::move(other.metadata_)){}};// 旧方式总是 const 引用voidprocessOld(constBigDatadata);// 新方式按值传递利用移动语义C11 起voidprocessNew(BigData data);// 调用者可以 move 进来// 使用BigData bigData;processNew(std::move(bigData));// 移动零拷贝processNew(BigData());// 直接从临时对象构造零拷贝现代建议对于可移动的类型在某些场景下按值传递配合std::move可以达到与引用传递相同的性能同时代码更简洁。但这需要谨慎使用且不适用于多态场景。五、实际应用场景5.1 场景游戏引擎中的实体处理classGameEntity{public:virtualvoidupdate(floatdeltaTime)0;virtualvoidrender()const0;virtual~GameEntity()default;};classPlayer:publicGameEntity{/* ... */};classEnemy:publicGameEntity{/* ... */};classItem:publicGameEntity{/* ... */};// ❌ 危险对象切割 性能低下voidupdateEntityBad(GameEntity entity);// 千万别这样// ✅ 正确保持多态零拷贝voidupdateEntityGood(constGameEntityentity);// ✅ 如果需要修改实体voidupdateEntityMutable(GameEntityentity);// 游戏主循环voidgameLoop(conststd::vectorstd::unique_ptrGameEntityentities){for(constautoentity:entities){updateEntityGood(*entity);// 多态调用高效传递}}5.2 场景图像处理中的大矩阵#includevectorclassImageMatrix{private:std::vectoruint8_tpixels_;intwidth_,height_;public:ImageMatrix(intw,inth):width_(w),height_(h),pixels_(w*h*3){}// 拷贝构造会复制整个像素数组ImageMatrix(constImageMatrixother)default;intwidth()const{returnwidth_;}intheight()const{returnheight_;}constuint8_t*data()const{returnpixels_.data();}};// ❌ 灾难4K 图像 (3840x2160x3 ≈ 24MB) 被完整拷贝voidapplyFilterBad(ImageMatrix image);// ✅ 高效只传递引用voidapplyFilterGood(constImageMatriximage);// ✅ 如果需要输出结果可以传入输出参数voidapplyFilterInPlace(constImageMatrixinput,ImageMatrixoutput);5.3 场景配置对象的传递classAppConfig{public:std::string appName;std::string logPath;std::string dbConnectionString;intmaxThreads;inttimeoutSeconds;std::vectorstd::stringpluginPaths;// ... 可能还有很多字段};// ❌ 每次调用都拷贝整个配置对象voidinitializeModuleBad(AppConfig config);// ✅ 只读访问用 const 引用voidinitializeModuleGood(constAppConfigconfig);// 应用启动时voidstartup(){AppConfig configloadConfigFromFile(app.conf);initializeModuleGood(config);// 模块 AinitializeModuleGood(config);// 模块 BinitializeModuleGood(config);// 模块 C// 零额外拷贝}六、决策流程图选择参数传递方式 │ ├─ 类型是内置类型int, double, char, bool, 指针等 │ └─ ✅ 按值传递 │ ├─ 类型是 STL 迭代器或函数对象 │ └─ ✅ 按值传递 │ ├─ 类型是小型且拷贝廉价的结构体 2-3 个指针大小 │ └─ ✅ 按值传递 │ ├─ 函数需要修改参数 │ ├─ 是 → 按非 const 引用传递 │ └─ 否 → 继续判断 │ ├─ 参数可能为 null │ ├─ 是 → 考虑 const 指针const T* │ └─ 否 → ✅ 按 const 引用传递const T七、总结传递方式适用场景优点缺点pass-by-value内置类型、迭代器、小型对象简单、安全无副作用拷贝开销、对象切割pass-by-reference-to-const大多数自定义类型零拷贝、防切割、const 安全语法稍复杂pass-by-reference需要修改参数可修改原对象副作用风险核心原则对于自定义类型默认使用const T对于内置类型和 STL 迭代器使用T如果涉及多态必须使用引用或指针C11 后对于可移动类型某些场景可以考虑按值传递配合std::move八、延伸阅读Effective C 条款03尽可能使用 constEffective C 条款19设计 class 犹如设计 typeEffective C 条款21必须返回对象时别妄想返回其 referenceC Core GuidelinesF.16 - For “in” parameters, pass cheaply-copied types by value and others by reference toconst《C Primer》第 6 章函数如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、评论 你的支持是我持续创作的动力