item26中说明对使用通用引用形参的函数,无论是独立函数还是成员函数,进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个条款探讨了几种通过避免在通用引用上重载的设计,或者通过限制通用引用可以匹配的参数类型,来实现所期望行为的方法。
放弃重载
为了解决这个问题,一个方法是完全避免重载。例如,如果你有两个不同行为的logAndAdd函数,你可以给它们不同的名字,如logAndAddName和logAndAddNameIdx。这种方法简单明了,但并不总是可行,特别是对于构造函数这样的情况,因为构造函数的名字是由类名决定的,不能随意更改。
传递const T&
另一种方法是使用const T&来代替通用引用。这种方式虽然可能没有通用引用那么高效,因为它不允许移动语义,但它可以确保更可预测的行为。例如,你可以将接受std::string的构造函数改为接受const std::string&,这样就不会与整数类型混淆了。
传值
还有一种方法是按值传递参数,这通常看起来像是降低了效率,但实际上可以通过移动语义优化性能。当你知道你将要拷贝对象时,直接传递值可以让编译器利用RVO(返回值优化)或NRVO(命名返回值优化),甚至是在某些情况下应用移动语义。
class Person {
public:
// 使用std::string按值传递
explicit Person(std::string n)
: name(std::move(n)) {}
// 整数索引构造函数保持不变
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
private:
static std::string nameFromIdx(int idx) {
// 实现从索引获取名字的逻辑
return "Name" + std::to_string(idx);
}
std::string name;
};
第一个构造函数接受std::string类型的参数,并使用std::move来转移所有权,允许编译器进行潜在的优化。第二个构造函数保持不变,接受整数作为参数并调用nameFromIdx函数来生成名字。由于std::string构造函数不会接受整型参数,所以这两个构造函数之间不会有冲突。如果用户尝试用整数初始化Person对象,那么将会调用正确的构造函数。对于std::string或者能够隐式转换为std::string的类型,将使用第一个构造函数。这样做既保证了代码的行为符合预期,又保持了良好的性能。
Tag Dispatch
Tag dispatch通过向函数传递一个额外的类型参数来帮助编译器选择正确的重载版本。这个类型参数通常是一个std::true_type或std::false_type,是标准库提供的类型,用于表示布尔值。这些类型在编译时已知,因此可以帮助编译器做出正确的决策。
logAndAdd 函数
有一个logAndAdd函数,能够处理两种情况:
当传入的是字符串或者可以转换为字符串的类型时,将该名字添加到全局数据结构。当传入的是整数时,使用这个整数作为索引来查找对应的名字,然后调用logAndAdd。
首先,需要定义两个实现函数logAndAddImpl,一个处理非整型,另一个处理整型。
//非整型实参:添加到全局数据结构中
template<typename T>
void logAndAddImpl(T&& name, std::false_type) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
//整型实参:查找名字并用它调用logAndAdd
void logAndAddImpl(int idx, std::true_type) {
logAndAdd(nameFromIdx(idx));
}
//辅助函数,从索引获取名字
std::string nameFromIdx(int idx) {
//实现从索引获取名字的逻辑
return "Name" + std::to_string(idx);
}
接下来,编写主函数logAndAdd,根据传入的类型选择正确的logAndAddImpl版本:
template<typename T>
void logAndAdd(T&& name) {
using UnrefType = typename std::remove_reference<T>::type;
logAndAddImpl(
std::forward<T>(name),
std::is_integral<UnrefType>()
);
}
这里的关键点在于std::remove_reference<T>::type。当T是左值引用时(如int&),std::is_integral<int&>()会返回false,因为引用不是整型。因此我们需要使用std::remove_reference来移除引用,从而正确地判断T是否为整型。
//全局数据结构
std::multiset<std::string> names;
//日志记录函数
void log(const std::chrono::system_clock::time_point&, const char*) {
//实现日志记录逻辑
}
//根据索引获取名字
std::string nameFromIdx(int idx) {
//实现从索引获取名字的逻辑
return "Name" + std::to_string(idx);
}
//非整型实参:添加到全局数据结构中
template<typename T>
void logAndAddImpl(T&& name, std::false_type) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
//整型实参:查找名字并用它调用logAndAdd
void logAndAddImpl(int idx, std::true_type) {
logAndAdd(nameFromIdx(idx));
}
//主函数,根据传入类型选择正确的logAndAddImpl版本
template<typename T>
void logAndAdd(T&& name) {
using UnrefType = typename std::remove_reference<T>::type;
logAndAddImpl(
std::forward<T>(name),
std::is_integral<UnrefType>()
);
}
int main() {
logAndAdd("Alice"); // 添加字符串
logAndAdd(42); // 通过索引查找名字
return 0;
}
通过这种方式,可以避免在通用引用上重载带来的问题,同时还能保持代码的简洁性和可读性。在C++中,当使用通用引用(T&&)时,尤其是与重载结合时,可能会导致一些意外的行为。为了解决这些问题,可以采用tag dispatch和std::enable_if来控制模板的启用条件。
std::enable_if 的基本形式
template<bool B, class T = void>
struct enable_if;
// 当 B 为 true 时,enable_if<B, T>::type 为 T
// 当 B 为 false 时,enable_if<B, T> 没有 type 成员
// 辅助函数,从索引获取名字
std::string nameFromIdx(int idx) {
// 实现从索引获取名字的逻辑
return "Name" + std::to_string(idx);
}
class Person {
public:
// 完美转发构造函数,仅当T不是Person或其派生类且不是整型时启用
template<typename T,typename = std::enable_if_t<!std::is_base_of<Person, std::decay_t<T>>::value&&!std::is_integral<std::remove_reference_t<T>>::value>>
explicit Person(T&& n):name(std::forward<T>(n)) {}
// 整型实参的构造函数
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
// 拷贝构造函数
Person(const Person& other)
: name(other.name) {}
// 移动构造函数
Person(Person&& other) noexcept
: name(std::move(other.name)) {}
private:
std::string name;
};
// 派生类示例
class SpecialPerson : public Person {
public:
//拷贝构造函数
SpecialPerson(const SpecialPerson& rhs)
: Person(rhs) {}
//移动构造函数
SpecialPerson(SpecialPerson&& rhs) noexcept
: Person(std::move(rhs)) {}
};
完美转发构造函数:
使用std::enable_if来限制模板的启用条件。
!std::is_base_of<Person, std::decay_t<T>>::value 确保T不是Person或其派生类。!std::is_integral<std::remove_reference_t<T>>::value 确保T不是整型。std::decay_t<T> 用于移除T的引用和cv限定符。
std::remove_reference_t<T> 用于移除T的引用。
整型实参的构造函数:直接处理整型参数,调用nameFromIdx函数来获取名字。
拷贝和移动构造函数:显式定义了拷贝和移动构造函数,确保它们不会被完美转发构造函数覆盖。
派生类:SpecialPerson 类继承自 Person,并显式定义了拷贝和移动构造函数,确保它们调用基类的相应构造函数。
std::enable_if 的基本形式如下:
template<bool B, class T = void>
struct enable_if;
// 当 B 为 true 时,enable_if<B, T>::type 为 T
// 当 B 为 false 时,enable_if<B, T> 没有 type 成员
在模板声明中,std::enable_if 通常用于 SFINAE(Substitution Failure Is Not An Error)规则,即如果模板实例化失败,则该模板不会被考虑为候选函数。
假设有一个 Person 类,有一个接受通用引用的构造函数,并且希望这个构造函数只对非 Person 类型及其派生类和非整型参数启用。使用 std::enable_if 来确保只有当传入的类型不是 Person 或其派生类,并且不是整型时,才启用该构造函数。
定义处理整型参数的构造函数:提供一个专门处理整型参数的构造函数。
使用 std::is_base_of 和 std::is_integral,std::is_base_of 用于检查类型是否是 Person 或其派生类。
std::is_integral 用于检查类型是否是整型。
使用std::decay 用于移除引用和 cv 限定符,确保类型比较时忽略这些修饰。
请记住:
- 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-
const
传递形参,按值传递形参,使用tag dispatch。 - 通过
std::enable_if
约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。 - 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。