为什么要重新审视 “传值”?
传统上,要实现 “左值拷贝、右值移动” 有两种方式,但都有缺陷:
- 重载方案:写两个函数(const T& 处理左值、T&& 处理右值),代码冗余(两份声明 / 实现 / 维护),目标代码也会生成两份;
- 通用引用方案:用模板 T&& + std::forward,虽只需一个函数,但模板需放头文件(膨胀)、支持类型多导致目标代码多、编译错误晦涩,还可能匹配意外类型。
能否用一个函数、不依赖通用引用,同时实现左值拷贝、右值移动? 可以 —— 用 “按值传递 + 移动语义”,但需明确适用条件。
传值 + 移动语义
1. 实现方式(以 addName 为例)
class Widget {
public:
void addName(std::string newName) { // 形参按值传递
names.push_back(std::move(newName)); // 函数内move进容器
}
private:
std::vector<std::string> names;
};
2. 原理
- 传左值(如已存在的 string 变量):形参
newName拷贝构造,再move进容器 → 总开销:1 次拷贝 + 1 次移动; - 传右值(如临时字符串):形参
newName移动构造,再move进容器 → 总开销:2 次移动; - 对比 “按引用方案(重载 / 通用引用)”:按引用是 “左值 1 次拷贝、右值 1 次移动”,传值多了 1 次移动(但移动成本低,可接受)。
3. 优势
- 源代码 / 目标代码都只有 1 个函数,无冗余、无模板复杂度;
- 避免通用引用的头文件膨胀、编译错误晦涩等问题;
- 效率接近按引用方案(仅多 1 次低成本的移动)。
传值的 4 个关键适用条件
- 仅 “考虑” 传值:传值开销略高(多 1 次移动),需权衡 “代码简洁性” 和 “极致性能”—— 若软件要求绝对最优性能,仍需用按引用方案;
- 仅对 “可拷贝” 形参:只可移动类型(如
std::unique_ptr)不适用 —— 传值会多 1 次移动(总 2 次),而重载只需 1 个接受右值引用的函数(总 1 次移动),更高效; - 仅对 “移动成本低” 的形参:若移动成本高(如大对象),额外 1 次移动的开销会抵消简洁性的好处;
- 仅对 “总是被拷贝” 的形参:若形参可能不被拷贝(如校验名字长度失败则不添加),传值的 “构造 + 析构形参” 开销会白白浪费(按引用可避免)。
额外复杂度:构造拷贝 vs 赋值拷贝
传值的开销分析还需区分 “形参是通过构造拷贝” 还是 “通过赋值拷贝”:
-
构造拷贝(如 addName):形参被构造后 move 进新容器,额外 1 次移动的开销可接受;
-
赋值拷贝(如 Password::changeTo):
- 传值:左值实参→形参拷贝构造(分配新内存),再 move 赋值给成员(释放旧内存),可能触发两次动态内存操作;
- 按引用:若成员内存足够(如旧 string 比新 string 长),可重用内存,避免内存分配 / 释放,开销远低于传值。
传值的固有风险:切片问题
传值仍保留 C++98 的经典问题 ——对象切片:
- 若形参是基类类型,传递派生类对象时,派生类的特征会被 “切掉”,仅保留基类部分;
- 因此,基类类型的形参绝对不能按值传递(必须用引用 / 指针)。
调用链的累积开销
若调用链中多个函数都用传值,每个函数多 1 次移动,整体开销会累积,可能从 “可接受” 变成 “无法忍受”—— 而按引用传递无此问题。
总结
- 传值的适用场景:仅针对 “可拷贝、移动成本低、无条件被拷贝、非基类类型” 的形参,此时传值实现简单、代码 / 目标代码少,效率接近按引用方案;
- 传值的代价:比按引用多 1 次移动,赋值拷贝场景可能额外触发内存分配 / 释放,调用链累积会放大开销;
- 传值的风险:基类形参传值会导致对象切片,只可移动类型传值不如重载高效;
- 权衡:C++11 后传值不再 “绝对不可用”,但需在 “代码简洁性” 和 “极致性能” 之间取舍,仅在满足所有适用条件时考虑。

3297

被折叠的 条评论
为什么被折叠?



