将模板类型参数声明为友元
新标准下,我们可以将模板类型参数设为友元:
Code:
template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
// ...
};
这里我们说,无论使用什么类型来实例化Bar
,这个类型都是一个友元。因此,对于某个名为Foo
的类型,Foo
将是Bar<Foo>
的朋友,Sales_data
是Bar<Sales_data>
的朋友,依此类推。
值得注意的是,即使友元通常必须是一个类或一个函数,但Bar
也可以使用内置类型进行实例化。这样的友谊是允许的,这样我们就可以用内置类型实例化诸如Bar
之类的类。
模版类型别名
新标准允许我们为类模板定义类型别名:
Code:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors is a pair<string, string>
这里我们将twin
定义为成员具有相同类型的pair
的同义词。twin
的用户只需要指定一次类型。
模板类型别名是一系列类的同义词:
Code:
twin<int> win_loss; // win_loss is a pair<int, int>
twin<double> area; // area is a pair<double, double>
正如我们在使用类模板时所做的那样,当我们使用twin
时,我们会指定我们想要的哪种特殊类型的twin
。
当我们定义模板类型别名时,我们可以固定一个或多个模板参数:
Code:
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books is a pair<string, unsigned>
partNo<Vehicle> cars; // cars is a pair<Vehicle, unsigned>
partNo<Student> kids; // kids is a pair<Student, unsigned>
这里我们已经将partNo
定义为pair
类型族的同义词,其中pair
的第二个成员是unsigned
。partNo
的用户为该pair
的第一个成员指定了一个类型,但不能指定第二个成员的类型。
模板函数的默认模板参数
正如我们可以为函数参数提供默认参数一样,我们也可以提供默认模板参数。在新标准下,我们可以为函数和类模板提供默认参数。而在早期版本中,仅允许使用类模板的默认参数。
作为一个例子。我们将定义一个默认使用标准库less
函数对象模版的函数compare
:
Code:
// compare has a default template argument, less<T>
// and a default function argument, F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
这里我们为模板提供了第二个类型参数,名为F
,它表示可调用对象的类型,并定义了一个新函数参数f
,它将被绑定到一个可调用的对象。
我们还为此模板参数及其相应的函数参数提供了默认值。默认模板参数指定compare
将使用标准库less
函数对象类,使用与compare
相同的类型参数进行实例化。默认函数参数表明f
将是F
类型的默认初始化对象。
当用户调用此版本的compare
时,用户可能会提供自己的比较操作,但不是必须这样做:
Code:
bool i = compare(0, 42); // uses less; i is -1
// result depends on the isbns in item1 and item2
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
第一个调用使用默认函数参数,该参数是less<T>
类型的默认初始化对象。在此调用中,T
为int
,因此对象的类型为less<int>
。compare
的这种实例化将使用less<int>
来进行比较。
在第二次调用中,我们传递compareIsbn
和两个Sales_data
类型的对象。当使用三个参数调用compare
时,第三个参数的类型必须是一个可调用的对象,它返回一个可转换为bool
的类型,并获取与前两个参数类型兼容的类型的参数。像往常一样,模板参数的类型是从它们相应的函数参数推导出来的。在此调用中,T
的类型推导为Sales_data
,F
推导为compareIsbn
的类型。
与函数默认参数一样,只有当某一模版参数右侧的所有参数都具有默认参数时,该模板参数才可以具有默认参数。
实例化的显式控制
在大型系统中,在多个文件中实例化相同模板的开销可能会变得很大。在新标准下,我们可以通过显式实例化来避免这种开销。显式实例化具有如下形式:
Code:
extern template declaration; // instantiation declaration
template declaration; // instantiation definition
其中declaration
是一个类或函数声明,且其所有模板参数都被相应模版定义中的模板参数替换。例如:
Code:
// instantion declaration and definition
extern template class Blob<string>; // declaration
template int compare(const int&, const int&); // definition
当编译器看到extern
模板声明时,它不会为该文件中的实例化生成代码。将实例化声明为extern
是一种承诺,即在程序的其他地方存在非extern
的实例化。对于给定的实例化,可能存在若干extern
声明,但该实例化必须仅有一个定义。
因为编译器在我们使用它时自动实例化模板,所以extern
声明必须出现在使用该实例化的任何代码之前:
Code:
// Application.cc
// these template types must be instantiated elsewhere in the program
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; // instantiation will appear elsewhere
// Blob<int> and its initializer_list constructor instantiated in this file
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1); // copy constructor instantiated in this file
int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere
Application.o
文件将包含Blob<int>
的实例化,以及该类的initializer_list
和拷贝构造函数。compare<int>
函数和Blob<string>
类不会在该文件中实例化。程序中的某些其他文件中必须有这些模板的定义:
Code:
// templateBuild.cc
// instantiation file must provide a (nonextern) definition for every
// type and function that other files declare as extern
template int compare(const int&, const int&);
template class Blob<string>; // instantiates all members of the class template
当编译器看到实例化定义(而不是声明)时,它会生成代码。因此,文件templateBuild.o
将包含用int
实例化的compare
和Blob<string>
类的定义。构建应用程序时,我们必须将templateBuild.o
与Application.o
文件链接起来。
注:对于每个实例化声明,程序中的某处必须有一个显式的实例化定义。
模板函数和尾随返回类型
当我们想让用户确定返回类型时,使用显式的模板参数来表示模板函数的返回类型。在其他情况下,需要显式表明模板参数会给用户带来负担而没有任何优势。例如,我们可能想要编写一个函数,该函数接受一对表示序列的迭代器,并返回对序列中元素的引用:
Code:
template <typename It>
??? &fcn(It beg, It end)
{
// process the range
return *beg; // return a reference to an element from the range
}
我们不知道我们想要返回的确切类型,但我们知道我们希望该类型是对我们正在处理的序列的元素类型的引用:
Code:
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&
在这里,我们知道我们的函数将返回*beg
,并且我们知道我们可以使用decltype(*beg)
来获取该表达式的类型。但是直到参数列表传给该函数时beg
才存在。要定义此函数,我们必须使用尾随返回类型。因为在参数列表之后出现尾随返回,它可以使用函数的参数:
Code:
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// process the range
return *beg; // return a reference to an element from the range
}
这里我们告诉编译器fcn
的返回类型与解引用其beg
参数返回的类型相同。解引用运算符返回一个左值,因此由decltype
推导出的类型是对beg
表示的元素类型的引用。因此,如果在字符串序列上调用fcn
,则返回类型将为string &
。如果序列是int
,则返回将为int &
。
引用折叠规则
如果我们间接地创建了对引用的引用,那么这些引用就会“折叠”。引用都会折叠以形成普通的左值引用类型。新标准扩展了折叠规则以包括右值引用。只有在对右值引用的右值引用的特定情况下,引用才会折叠以形成右值引用。也就是说,对于给定的类型X
:
-
X& &
,X& &&
和X&& &
都会折叠为类型X&
。 - 类型
X&& &&
折叠为X&&
。
注:仅当间接创建对引用的引用时(例如在类型别名或模板参数中),才会应用引用折叠。
Code:
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
参考折叠规则和右值参考参数类型推导的特殊规则的组合意味着我们可以在左值上调用f3
。当我们将左值传递给f3
(右值引用)函数参数时,编译器会将T
推导为左值引用类型:
Code:
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
当模板参数T
被推导为引用类型时,折叠规则表示函数参数T&&
折叠为左值引用类型。例如,f3(i)
的实例化结果将类似于:
Code:
// invalid code, for illustration purposes only
void f3<int&>(int& &&); // when T is int&, function parameter is int& &&
f3
中的函数参数是T&&
,T
是int&
,因此T&&
是int& &&
,它会折叠为int&
。因此,即使f3
中函数参数的形式是右值引用(即T &&
),该调用也使用左值引用类型(即int&
)实例化f3
:
Code:
void f3<int&>(int&); // when T is int&, function parameter collapses to int&
对于这些规则有两个重要的结论:
- 作为模板类型参数(例如,
T &&
)的右值引用的函数参数可以绑定到左值。 - 如果参数是左值,则推导出的模板参数类型将是左值引用类型,函数参数将被实例化为(普通)左值引用参数(
T&
)。
值得注意的是,我们可以将任何类型的参数传递给T&&
函数参数。这种类型的函数参数(显然)可以与右值一起使用,正如我们刚才看到的,也可以被左值使用。
注:可以将任何类型的参数传递给函数参数,该参数是对模板参数类型(即T&&
)的右值引用。当左值传递给这样的参数时,函数参数被实例化为普通的左值引用(T&
)。
从左值到右值的static_cast
通常,static_cast
只能执行其他合法的转换。然而,对于右值引用又有一个特殊的分配:即使我们不能隐式地将左值转换为右值引用,我们也可以使用static_cast
显式地将左值转换为右值引用。
将右值引用绑定到左值会给出对右值引用权限进行操作的代码,以破坏左值。通常编译器允许我们进行这样的转换,当我们知道破坏左值是安全的,我们就可以进行这样的转换。但是编译器强制我们使用显示的方式进行这样的转换,目的是防止我们意外地进行这样的转换操作。
最后,虽然我们可以直接编写这样的强制转换,但使用标准库move
函数要容易得多。此外,使用std::move
可以很容易地在代码中找到可能会破坏左值的位置。
标准库函数forward
与move
相同,forward
标准库函数在头文件utility
中定义;与move
不同,必须使用显示模版参数调用forward
。forward
返回对该显式参数类型的右值引用。也就是说,forward<T>
的返回类型是T&&
。
通常,我们使用forward
来传递一个函数参数,该参数被定义为模板类型参数的右值引用。通过引用折叠其返回类型,forward
保留其给定参数的左值/右值性质:
Code:
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
这里我们使用从arg
推导出的Type
作为forward
的显式模板参数类型。因为arg
是对模板类型参数的右值引用,所以Type
将表示传递给arg
的参数中的所有类型信息。如果该参数是右值,则Type
是普通(非引用)类型,而forward<Type>
将返回Type&&
。如果参数是左值,则通过引用折叠,Type
本身是左值引用类型。在这种情况下,返回类型是左值引用类型的右值引用。再次通过引用折叠,此时forward<Type>
的返回类型是左值引用类型。
注:当与作为模板类型参数(T&&
)的右值引用的函数参数一起使用时,forward
会保留有关参数类型的所有详细信息。
使用forward
,我们定义flip
函数:
Code:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
如果我们调用flip(g, i, 42)
,i
将以类型int&
传递给g;42将以类型int&&
传递给g。
注:与std::move
类似,对于std::forward
最好不提供using
声明
关于forward
更多信息,可参考std::forward。
可变参数模版(Variadic Templates)
可变参数模板是可以采用不同数量参数的模板函数或类。变化的参数称为参数包。有两种参数包:模板参数包表示零个或多个模板参数;函数参数包表示零个或多个函数参数。
我们使用省略号来指示模板或函数参数表示包。在模板参数列表中,class...
或typename...
表示以下参数代表零个或多个类型的列表;后跟省略号的类型名称表示给定类型的零个或多个非类型参数的列表。在函数参数列表中,类型为模板参数包的参数是函数参数包。例如:
Code:
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
上述代码中声明foo
是一个可变参数函数,它有一个名为T
的类型参数和一个名为Args
的模板参数包。该包表示零个或多个其他类型参数。foo
的函数参数列表有一个参数,其类型是一个const T &
( 其中T
可以是任意类型),以及一个名为rest
的函数参数包,该包表示零个或多个函数参数。
通常,编译器从函数的参数中推导出模板参数类型。对于可变参数模板,编译器还会推导出包中的参数数量。例如,下面的这些调用:
Code:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // three parameters in the pack
foo(s, 42, "hi"); // two parameters in the pack
foo(d, s); // one parameter in the pack
foo("hi"); // empty pack
编译器将实例化foo
的四个不同实例:
Code:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
在每种情况下,T
的类型都是从第一个参数的类型推导出来的。其余参数(如果有)提供函数的附加参数的数量和类型。
sizeof...操作
当我们需要知道包中有多少元素时,我们可以使用sizeof...
运算符。与sizeof
一样,sizeof...
返回一个常量表达式并且不评估其参数:
Code:
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // number of type parameters
cout << sizeof...(args) << endl; // number of function parameters
}
可变参数模版和转发
在新标准下,我们可以使用可变参数模板和forward
来编写将其参数不变地传递给其他函数的函数。 为了说明这些函数,我们将向StrVec
类中添加一个emplace_back
成员。标准库容器的emplace_back
成员是一个可变参数模板,它使用其参数直接在容器管理的空间中构造元素。
我们的StrVec
版本的emplace_back
也必须是可变参数,因为string
有许多构造函数,它们的参数不同。因为我们希望能够使用字符串移动构造函数,所以我们还需要保留有关传递给emplace_back
的参数的所有类型信息。
正如我们所见,保留类型信息是一个两步过程。首先,为了保留参数中的类型信息,我们必须将emplace_back
的函数参数定义为对模板类型参数的右值引用:
Code:
class StrVec {
public:
template <class... Args> void emplace_back(Args&&...);
// remaining members as in § 13.5 (p. 526)
};
模板参数包中的模式&&
意味着每个函数参数都是对应参数的右值引用。
其次,当emplace_back
将这些参数传递给construct
时,我们必须使用forward
来保留参数的原始类型:
Code:
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args)
{
chk_n_alloc(); // reallocates the StrVec if necessary
alloc.construct(first_free++, std::forward<Args>(args)...);
}
emplace_back
的主体调用chk_n_alloc
以确保有足够的空间并调用construct
在first_free
中创建元素。对于construct
中:
Code:
std::forward<Args>(args)...
扩展模板参数包Args
和函数参数包args
。此模式生成如下格式的元素:
Code:
std::forward<Ti>(ti)
其中Ti
表示模板参数包中第i
个元素的类型,ti
表示函数参数包中的第i
个元素。例如,假设svec
是一个StrVec
对象,如果我们调用:
Code:
svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element
调用construct
中的模式将扩展为:
Code:
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用forward
,我们保证如果使用右值调用emplace_back
,那么construct
也会得到一个右值。例如,在下面的调用中:
Code:
svec.emplace_back(s1 + s2); // uses the move constructor
emplace_back
的参数是一个右值,它被传递给construct
为:
Code:
std::forward<string>(string("the end"))
forward<string>
的结果类型是string&&
,因此将使用右值引用调用construct
。反过来,construct
函数会将此参数转发给字符串移动构造函数以构建此元素。
注:
变参数函数通常将其参数转发给其他函数。这些函数通常具有类似于emplace_back
函数的形式:
Code:
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... Args>
void fun(Args&&... args) // expands Args as a list of rvalue references
{
// the argument to work expands both Args and args
work(std::forward<Args>(args)...);
}
在这里,我们希望将所有fun
的参数转发给另一个名为work
的函数,该函数可能完成函数的实际工作。就像我们调用construct
内部emplace_back
一样,调用work
的扩展,扩展了模板参数包和函数参数包。
因为fun
的参数是右值引用,所以我们可以将任何类型的参数传递给fun
;因为我们使用std::forward
来传递这些参数,所以关于这些参数的所有类型信息都将在对work
的调用中保留。
参考文献
[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.