《Effective Modern C++》学习笔记 - Item 26: 避免对万能引用参数进行重载

本文探讨了在函数重载中使用万能引用参数可能导致的性能问题,特别是在处理不同类型参数和构造函数重载时。通过实例和类的设计,揭示了这种参数如何意外劫持调用,以及在处理类的完美转发构造函数时遇到的额外挑战。解决策略和替代方法将在第27节中讨论。
摘要由CSDN通过智能技术生成
  • 假设有下面这样一个函数,它将一个字符串参数 name 加入到全局数据集合 names 中,同时记录当前时间:
std::multiset<std::string> names; 			// global data structure
void logAndAdd(const std::string& name)
{
	auto now = 								// get current time
	std::chrono::system_clock::now();
	log(now, "logAndAdd"); 					// make log entry
	names.emplace(name); 					// add name to global data
	// 注:emplace接受若干个参数,并用这些参数直接在容器中构造一个新元素
	// 即调用元素接受这些参数版本的构造函数
}

上面的写法很常见,但在性能上还有提升空间。考虑以下三种调用:

std::string petName("Darla");
logAndAdd(petName); 					// pass lvalue std::string
logAndAdd(std::string("Persephone")); 	// pass rvalue std::string
logAndAdd("Patty Dog"); 				// pass string literal
  1. 第一个调用中,入参是一个左值,置入容器时需要一次拷贝(emplace 内部构造时调用 string 的拷贝构造函数),行为正确且已无法进一步优化。
  2. 第二个调用中,入参是一个右值;然而函数参数 name 的声明类型是左值(const string&),所以在置入容器时仍进行一次拷贝操作,而非移动。
  3. 第三个调用中,入参是一个字符串字面值(string literal)。尽管字面值可以直接被用在 emplace 中构造 string,这里实际发生的是:先隐式地用字面值构造一个临时 string 对象作为入参调用,再在置入时进行一次拷贝操作。
  • 使用万能引用参数可以解决以上所有效率低下的缺陷:
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 petName("Darla");
logAndAdd(petName);	 						// as before, copy lvalue into multiset
logAndAdd(std::string("Persephone")); 		// move rvalue instead of copying it
logAndAdd("Patty Dog"); 					// create std::string in multiset instead
 											// of copying a temporary std::string
  • 问题到此没有结束。假设现在有一个新需求:用户可能手里没有 name,而是一个查找 name 的索引 idx。为了支持,我们需要添加一个重载版本的 logAndAdd
std::string nameFromIdx(int idx); 	// return name corresponding to idx
void logAndAdd(int idx) 			// new overload
{
	auto now = std::chrono::system_clock::now();
	log(now, "logAndAdd");
	names.emplace(nameFromIdx(idx));
}

之前的所有调用都原样运行,一个新调用 logAndAdd(22) 的行为也如同预期,调用了新的重载版本。问题是,假设入参不是一个准确的 int 型,而比如是 short

short nameIdx;
... 				// give nameIdx a value
logAndAdd(nameIdx); // compile error!

结果是编译失败!原因是,两个重载版本,T&& 版可以实例化为 short 类型参数匹配,而 int 版需要一次类型转换参数才匹配。二者相比,编译器会选择前者,emplace 中试图用 short 去 构造一个 string,结果自然是失败。 当用 int 参数调用时,两个版本都不用类型转换,编译器基于普通函数优先于模板函数实例化的原则选择了后者。

  • 至此你应该已经能看出问题所在:万能引用参数太“贪婪”了——它能实例化以准确匹配几乎任何参数类型(例外在 Item 30 中描述),因此想要将其和重载同时使用通常是一个坏主意:万能引用版本会劫走远多于开发者预期的调用。

  • 很容易踩进这个坑的场景是类的完美转发的构造函数。假设有一个类 Person,其有一个 string 成员 name

class Person {
public:
	template<typename T>
	explicit Person(T&& n) 			// perfect forwarding ctor;
	: name(std::forward<T>(n)) {} 	// initializes data member
	explicit Person(int idx) 		// int ctor
	: name(nameFromIdx(idx)) {}
private:
 std::string name;
};

上例同样的问题仍然存在,而且还有更糟糕的:Item 17 描述了编译器会自动生成特殊函数,包括拷贝和移动构造函数

	Person(const Person& rhs); 	// copy ctor (compiler-generated)
	Person(Person&& rhs); 		// move ctor (compiler-generated)

万能引用参数版本甚至会影响这些函数!例如,一个拷贝构造:

Person p("Nancy");
auto cloneOfP(p); 	// create new Person from p;
 					// this won't compile!

竟然会编译失败!而原因与之前相同:p 是一个 Person,拷贝构造函数的参数是 const Person&,需要一次 const 转换,而万能引用版本不需要,所以编译器会选择后者。这等同于试图用 Person 对象构造 string,导致调用失败。只有当 p 的类型准确为 const Person,编译器才会正确地调用拷贝构造版本。

  • 如果再加上继承关系,问题还会更多:
class SpecialPerson: public Person {
public:
	SpecialPerson(const SpecialPerson& rhs) // copy ctor; 
	: Person(rhs)							// calls base class forwarding ctor!
	{ ... }
	SpecialPerson(SpecialPerson&& rhs) 		// move ctor;
	: Person(std::move(rhs)) 				// calls base class forwarding ctor!
	{ ... } //
};

派生类的拷贝构造中对基类的拷贝又错误地调用了万能引用版本的构造函数,这次是因为参数 rhs 的类型是 const SpecialPerson& 而非 const Person&——拷贝构造版本的匹配还是需要类型转换!

  • 既然对万能引用参数进行重载是个坏主意,那么如果需要一个函数既能转发大部分类型参数,又需要特殊处理某几种类型参数该怎么办呢?请见下一节 Item 27。

总结

  1. 对万能引用参数进行重载几乎总会导致万能引用的原版本被调用得比预期更频繁。
  2. 完美转发构造函数的问题尤其多,因为它们通常会劫走拷贝和移动构造函数的 non-const 左值参数的调用和派生类参数的调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值