条款27:熟悉万能引用重载的替代方法
条款27:熟悉万能引用重载的替代方法
背景示例
std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string nameFromIdx(int idx);
void logAndAdd(int idx) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
问题:logAndAdd(short) 会调用 T&& 版本,而不是 int 重载,导致错误或不符合预期行为。
在 条款26 中提到,万能引用(T&&,即同时能绑定左值和右值的引用)用于函数重载(尤其构造函数)时,极易造成重载解析混乱或意外调用。本条款探索几种替代手段来避免这种困境。
问题本质
- 万能引用
T&&太“贪婪”,几乎能匹配所有类型(包括左值、右值、非目标类型如short)。 - 容易导致意外的重载解析结果。
- 尤其在构造函数中(如
Person(T&&)和Person(int)),容易引发歧义甚至编译失败。
替代方案
放弃重载,分开命名
做法:将函数名拆开,例如 logAndAddName() 和 logAndAddIndex()。
void logAndAddName(std::string name) {
names.emplace(std::move(name));
}
void logAndAddIndex(int idx) {
names.emplace(nameFromIdx(idx));
}
- 适用场景:函数非构造函数时。
- 限制:构造函数名称不能改,不适用。
传 const T&
void logAndAdd(const std::string& name) {
names.emplace(name);
}
- 优点:无重载问题,行为稳定。
- 缺点:性能较差,不能完美转发右值。
const T&接收右值时,不会触发移动构造,而是执行拷贝构造。- 对于像
std::string这种 可以移动优化 的类型,会浪费一次资源分配。 - 所以它的性能低于支持完美转发(如
T&&)或按值传递(可以std::move)的写法。
传值(按值传参)
void logAndAdd(std::string name) {
names.emplace(std::move(name));
}
-
优点:可接收左值/右值,内部手动
move,效率不差。- 可接收左值或右值:不像
const std::string&只能“延长生命周期”或避免拷贝,按值传参的版本会对左值执行拷贝,对右值执行移动(由编译器自动判断调用时该拷还是移)。 - 内部
std::move提升效率:即使我们传了个左值,name是局部变量了,我们可以放心地对它std::move(),减少一次拷贝。举个例子:
std::string s = "hello"; logAndAdd(s); // 拷贝构造 name,然后 move 进 names logAndAdd(std::string("world")); // 移动构造 name,然后再 move 进 names在 C++ 中,无论是调用函数还是构造对象,实参总是先被用来初始化形参(函数的局部变量)。这个“初始化”的过程,就是:
- 如果实参是左值 → 拷贝构造形参
- 如果实参是右值 → 尝试使用移动构造形参(如果可用)
- 效率不差:过去很多人觉得“按值传参”性能低,其实这在现代 C++ 里不一定成立(尤其对
std::string这种移动代价远低于拷贝的类型)。
- 可接收左值或右值:不像
-
推荐用于值语义明确的类型。所谓值语义明确,指的是像
std::string这样的类型:- 拷贝/移动语义清晰(不像
std::unique_ptr这种只能移动); - 没有共享资源;
- 拷贝或移动是安全和常见的。
- 因此,对于
std::string、std::vector、std::optional这类类型,按值传参 +std::move()使用是现代 C++ 的推荐写法。
- 拷贝/移动语义清晰(不像
标签分派(Tag Dispatch)
分发入口
template<typename T>
void logAndAdd(T&& name) {
// 根据 T 是否为整型类型选择对应的重载版本(true_type 或 false_type)
logAndAddImpl(
std::forward<T>(name), // 完美转发实参(可能是左值或右值)
std::is_integral<typename std::remove_reference<T>::type>()
// std::remove_reference 移除引用修饰符,确保能正确识别基础类型
// is_integral 是 type trait,判断是否为整型类型
);
}
非整型重载
template<typename T>
void logAndAddImpl(T&& name, std::false_type) {
// 非整型类型走这里,比如 std::string、const char* 等
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd"); // 记录日志
names.emplace(std::forward<T>(name)); // 完美转发 name 到 emplace,提高效率
}
整型重载
void logAndAddImpl(int idx, std::true_type) {
// 整型类型走这里(如 int、short 等)
// 通过索引查找对应名字,然后调用 logAndAdd(name)
logAndAdd(nameFromIdx(idx)); // 递归调用,实参现在是 std::string,走 false_type 分支
}
- 优点:实现完美转发,兼容重载。
- 缺点:模板复杂,学习成本高。
使用 std::enable_if 限制模板适用范围
class Person {
public:
// 完美转发构造函数(模板)
template<
typename T,
// 使用 SFINAE 限制该模板仅在以下条件下启用:
typename = typename std::enable_if<
// ✅ 条件1:T 不是 Person 或其派生类
!std::is_base_of<Person, typename std::decay<T>::type>::value &&
// ✅ 条件2:T 不是整型类型(如 int、short 等)
!std::is_integral<typename std::remove_reference<T>::type>::value
>::type
>
explicit Person(T&& n)
// 使用 std::forward 完美转发 n,避免不必要拷贝
: name(std::forward<T>(n)) {}
// 非模板重载:用于整型参数,通过索引查找对应姓名
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
// 拷贝构造函数(用于 Person 左值对象)
Person(const Person& rhs) = default;
// 移动构造函数(用于 Person 右值对象)
Person(Person&& rhs) = default;
private:
std::string name;
};
解释:
- 屏蔽了
int类型等整型。 - 屏蔽了
Person自身和其派生类(防止拷贝/移动调用错T&&构造)。 - 保留了对
std::string、字符串字面量等类型的完美转发。
加分项:添加 static_assert,提高错误信息友好性
在模板构造函数体内添加一个编译时断言 static_assert,用于检查传入的参数 T 是否能用来构造一个 std::string 对象。
template<
typename T,
typename = typename std::enable_if<
!std::is_base_of<Person, typename std::decay<T>::type>::value &&
!std::is_integral<typename std::remove_reference<T>::type>::value
>::type
>
explicit Person(T&& n)
: name(std::forward<T>(n))
{
static_assert(
std::is_constructible<std::string, T>::value,
"Parameter n can't be used to construct a std::string"
);
// 其他构造函数体内容(如果有)
}
std::is_constructible<std::string, T>::value
判断是否可以用类型T的对象来构造一个std::string。- 如果 不能构造,就会触发编译错误,编译器会输出下面自定义的错误信息:
"Parameter n can't be used to construct a std::string" - 这样做的目的是:
- 避免用户遇到非常冗长、难以理解的模板错误(可能会有 100+ 行报错)
- 直接给出一个明确且简洁的错误提示,方便定位问题
万能引用构造函数的陷阱:
Person p("Nancy"); // T = const char(&)[6] → 允许
Person p(u"Zuse"); // T = const char16_t(&)[5] → 无法构造 string → 巨大错误信息
总结
| 技术方案 | 是否支持完美转发 | 是否支持重载 | 性能 | 错误信息友好度 |
|---|---|---|---|---|
| 放弃重载(改名) | ❌ | ✅(绕过) | 高 | 高 |
const T& |
❌ | ✅ | 一般 | 高 |
| 按值 | ❌ | ✅ | 较高 | 高 |
| 标签分派 | ✅ | ✅ | 高 | 一般 |
enable_if 限制模板 |
✅ | ✅ | 高 | 一般~低(可用 static_assert 改善) |

浙公网安备 33010602011771号