C++11 中可变参数模板的引入使得必须确保有一种方式,能够将函数调用点的语义在整个调用链中传递下去。这听起来可能有些抽象,但实际上非常重要,并且对函数调用的效果具有影响。
考虑以下类:
#include <string>
struct X {
X(int, const std::string&); // A
X(int, std::string&&); // B
// ... 其他构造函数和各种成员
};
这个类至少暴露了两个构造函数:一个接受 int 和 const string& 作为参数,另一个接受 int 和 string&&。为了使示例更具一般性,还可以假设存在其他 X 的构造函数,我们可能也想调用它们,同时仍然关注这两个构造函数。如果我们显式地调用这两个构造函数,可以按以下方式进行:
X x0{ 3, "hello" }; // 调用 A
string s = "hi!";
X x1{ 4, s }; // 同样调用 A
X x2{ 5, string{ "there" } }; // 调用 B
X x3{ 5, "there too"s }; // 同样调用 B
x0 的构造函数调用了 A,因为 "hello" 是一个 const char(&)[6] 类型(包括结尾的 \0),而不是 string 类型,但编译器在这种情况下允许生成一个临时的 string 对象,并以 const string& 的形式传递(如果参数是 string& 而不是 const string&,则不能这样做,因为这会要求绑定到一个可修改的对象)。
x1 的构造函数也调用了 A,因为 s 是一个具名的 string 对象,这意味着它不能被隐式地以移动的方式传递。
x2 和 x3 的构造函数都调用了 B,它接受一个 string&& 作为参数,因为它们所传递的都是临时的、无名的 string 对象,可以被隐式地以移动的方式传递。
假设想编写一个 X 对象的工厂函数,用于在执行一些初步操作(比如在这个例子中记录我们正在构造一个 X 对象)之后,将参数转发给合适的 X 构造函数(可以是正在讨论的两个之一,也可以是其他任何 X 构造函数)。假设这样编写:
template <class ... Args>
X makeX(Args ... args) {
clog << "Creating a X object\n";
return X(args...); // <-- HERE
}
在这种情况下,所有参数都会被命名并按值传递,因此不会选择接受 string&& 的构造函数。
现在,这样编写:
template <class ... Args>
X makeX(Args &... args) {
clog << "Creating a X object\n";
return X(args...); // <-- HERE
}
这种情况下,所有参数都会以引用方式传递,此时如果有一个调用传递的是字符数组(如 "hello"),则将无法通过编译。我们需要做的是编写工厂函数,使其每个参数都能保留它们在调用点所具有的语义,并且函数能够以完全相同的语义将这些参数转发出去。
在 C++ 中实现这一点的方式涉及“转发引用(forwarding references)”以及一个特殊的库函数 std::forward<T>()(定义在 <utility> 中),表现为一种类型转换。转发引用在表面上和语法上看起来很像用于移动语义的右值引用,但它们对参数语义的影响却大不相同。考虑以下示例:
// v 通过移动方式传递(vector<int> 类型已完全指定)
void f0(vector<int> &&v);
// v 通过移动方式传递(vector<T> 类型已完全指定,T 为某种类型)
template <class T>
void f1(vector<T> &&v);
// v 是一个转发引用(类型由编译器推导)
template <class T>
void f2(T&& v);
使用转发引用时,参数的语义取决于调用点。例如,有如下函数 f2():
// T 是 vector<int>&& (通过移动方式传递)
f2(vector<int>{ 2,3,5,7,11 });
vector<int> v0{ 2,3,5,7,11 };
f2(v0); // T 是 vector<int>& (通过引用传递)
const vector<int> v1{ 2,3,5,7,11 };
f2(v1); // T 是 const vector<int>& (通过常量引用传递)
回到我们创建 X 对象的工厂函数,在这种情况下,makeX() 的合适函数签名如下所示:
template <class ... Args>
X makeX(Args &&... args) {
clog << "Creating a X object\n";
return X(args...); // <-- HERE (还是不正确)
}
这个版本的函数可以正常工作。makeX() 的签名是正确的,因为每个参数都会以调用点所使用的类型接受,无论是左值引用、const 左值引用,还是右值引用。但缺失的关键点是:现在作为右值引用接收到的参数在 makeX() 函数内部已经有了名字(名为 args... 的参数包的一部分),在调用 X 的构造函数时,不再会触发隐式的移动操作。
为了完成我们的目标,需要将每个参数重新转换为它在调用点所具有的类型。这个类型信息已经记录在 Args 中(即参数包的类型),而执行这种类型转换的方式就是对参数包中的每个参数应用 std::forward<T>()。最终,一个正确的 makeX() 函数应该是这样的:
template <class ... Args>
X makeX(Args &&... args) {
clog << "Creating a X object\n";
return X(std::forward<Args>(args)...); // <-- HERE
}
呼!确实还有更简单的语法形式,但我们终于完成了。