第 5 章 基础技巧
5.1 typename 关键字
关键字typename在C++标准化过程中被引入进来,用来澄清模板内部的一个标识符代表的
是某种类型,而不是数据成员。考虑下面这个例子:
template<typename T>
class MyClass {
public:void foo() {typename T::SubType* ptr;
}
};
其中第二个 typename 被用来澄清 SubType 是定义在 class T 中的一个类型。因此在这里 ptr
是一个指向 T::SubType 类型的指针。
如果没有 typename 的话,SubType 会被假设成一个非类型成员(比如 static 成员或者一个枚举常量,亦或者是内部嵌套类或者 using 声明的 public 别名)。这样的话,表达式
T::SubType* ptr 会被理解成 class T 的 static 成员 SubType 与 ptr 的乘法运算,这不是一个错误,因为对 MyClass<>的某些实例化版本而言,这可能是有效的代码。
通常而言,当一个依赖于模板参数的名称代表的是某种类型的时候,就必须使用 typename。
13.3.2 节会对这一内容做进一步的讨论。
使用 typename 的一种场景是用来声明泛型代码中标准容器的迭代器:
// print elements of an STL container
template<typename T>
void printcoll(T const& coll)
{typename T::const_iterator pos; // iterator to iterate over colltypename T::const_iterator end(coll.end()); // end positionfor (pos = coll.begin(); pos != end; ++pos) {std::cout << *pos << "";}std::cout << "\n";
}int main()
{std::string test = "hello";printcoll(test);return 0;
}
5.2零初始化
对于基础类型,比如int,double以及指针类型,由于它们没有默认构造函数,因此它们不
会被默认初始化成一个有意义的值。比如任何未被初始化的局部变量的值都是未定义的:
void foo()
{int x; // x has undefined valueint* ptr; // ptr points to anywhere (instead of nowhere)
}
因此在定义模板时,如果想让一个模板类型的变量被初始化成一个默认值,那么只是简单的
定义是不够的,因为对内置类型,它们不会被初始化:
template<typename T>
void foo()
{T x; // x has undefined value if T is built-in type
}
正确做法
void foo()
{int x{}; // x has undefined valueint* ptr{}; // ptr points to anywhere (instead of nowhere)std::cout << x << " " << ptr;
}
出于这个原因,对于内置类型,最好显式的调用其默认构造函数来将它们初始化成 0(对于
bool 类型,初始化为 false,对于指针类型,初始化成 nullptr)。通过下面你的写法就可以
保证即使是内置类型也可以得到适当的初始化:
template<typename T>
void foo()
{T x{}; // x is zero (or false) if T is a built-in type
}
这种初始化的方法被称为“值初始化(value initialization)”,它要么调用一个对象已有的
构造函数,要么就用零来初始化这个对象。即使它有显式的构造函数也是这样。
对于用花括号初始 化的情况,如果没有可用的默认构造函数,它还可以使用列表初始化构造函数(initializer-list constructor)。
从 C++11 开始也可以通过如下方式对非静态成员进行默认初始化:
template<typename T>
class MyClass {
private:
T x{}; // zero-initialize x unless otherwise specified …
};
模版参数默认值
template<typename T>
void foo(T p = T{}) { //OK (must use T() before C++11) …
}
5.3 使用 this->
对于类模板,如果它的基类也是依赖于模板参数的,那么对它而言即使 x 是继承而来的,使
用 this->x 和 x 也不一定是等效的。比如:
template<typename T>
class Base {
public:void bar();
};template<typename T>
class Derived : Base<T> {
public:void foo() {bar(); // calls external bar() or error}
};
Derived 中的 bar()永远不会被解析成 Base 中的 bar()。因此这样做要么会遇到错误,要么就
是调用了其它地方的 bar()(比如可能是定义在其它地方的 global 的 bar())。
13.4.2 节对这一问题有更详细的讨论。目前作为经验法则,建议当使用定义于基类中的、依
赖于模板参数的成员时,用 this->或者 Base<T>::来修饰它。
5.4 使用裸数组或者字符串常量的模板
5.5 成员模板
Stack<int> intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats…intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR: stacks have different types
template<typename T>
class Stack {
private:std::deque<T> elems; // elements
public:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();}// assign stack of elements of type T2template<typename T2>Stack& operator= (Stack<T2> const&);
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{Stack<T2> tmp(op2); // create a copy of the assigned stackelems.clear(); // remove existing elementswhile (!tmp.empty()) { // copy all elementselems.push_front(tmp.top());tmp.pop();}return *this;
}
成员模板的特例化
成员函数模板也可以被全部或者部分地特例化。比如对下面这个例子:
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>
#include <deque>class BoolString {
private:std::string value;
public:BoolString(std::string const& s): value(s) {}template<typename T = std::string>T get() const { // get value (converted to T)return value;}template<>inline bool get<bool>() const {return value == "true" || value == "1" || value == "on";}
};int main()
{std::cout << std::boolalpha;BoolString s1("hello");std::cout << s1.get() << "\n"; //prints hellostd::cout << s1.get<bool>() << "\n"; //prints falseBoolString s2("on");std::cout << s2.get<bool>() << "\n"; //prints truereturn 0;
}
特殊成员函数的模板
template 的使用
#include <bitset>template<unsigned long N>
void printBitset(std::bitset<N> const& bs) {std::cout << bs.template to_string<char,std::char_traits<char>,std::allocator<char>>();
}
泛型 lambdas 和成员模板
在 C++14 中引入的泛型 lambdas,是一种成员模板的简化。对于一个简单的计算两个任意类
型参数之和的 lambda:
[] (auto x, auto y) {return x + y;
}
编译器会默认为它构造下面这样一个类:
class SomeCompilerSpecificName {
public:SomeCompilerSpecificName(); // constructor only callable by compilertemplate<typename T1, typename T2>auto operator() (T1 x, T2 y) const {return x + y;}
};
5.6 变量模板
用于数据成员的变量模板
template<typename T>
class MyClass {
public:static constexpr int max = 1000;
};
namespace std {
template<typename T>
class numeric_limits {public: …static constexpr bool is_signed = false; …
};
}
类型萃取 Suffix_v
5.7 模板参数模板
#include<deque>template<typename T,template<typename Elem> class Cont = std::deque>
class Stack {
private:Cont<T> elems; // elements
public:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} …
};
模板参数模板的参数匹配
template<typename T, template<typename Elem,
typename Alloc = std::allocator<Elem>> class Cont = std::deque>
class Stack {
private:Cont<T> elems; // elements
…
};
#include <iostream>
#include <deque>
#include <cassert>
#include <memory>
#include <vector>template<typename T, template<typename Elem, typename =std::allocator<Elem>> class Cont = std::deque>class Stack {private:Cont<T> elems; // elementspublic:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();}// assign stack of elements of type T2template<typename T2, template<typename Elem2,typename = std::allocator<Elem2> >class Cont2>Stack<T, Cont>& operator= (Stack<T2, Cont2> const&);// to get access to private members of any Stack with elements of type T2 :template<typename, template<typename, typename>class>friend class Stack;
};template<typename T, template<typename, typename> class Cont>
void Stack<T, Cont>::push(T const& elem)
{elems.push_back(elem); // append copy of passed elem
}
template<typename T, template<typename, typename> class Cont>
void Stack<T, Cont>::pop()
{assert(!elems.empty());elems.pop_back(); // remove last element
}
template<typename T, template<typename, typename> class Cont>
T const& Stack<T, Cont>::top() const
{assert(!elems.empty());return elems.back(); // return copy of last element
}
template<typename T, template<typename, typename> class Cont>
template<typename T2, template<typename, typename> class Cont2>
Stack<T, Cont>&
Stack<T, Cont>::operator= (Stack<T2, Cont2> const& op2)
{elems.clear(); // remove existing elementselems.insert(elems.begin(), // insert at the beginningop2.elems.begin(), // all elements from op2op2.elems.end());return *this;
}int main()
{Stack<int> iStack; // stack of intsStack<float> fStack; // stack of floats// manipulate int stackiStack.push(1);iStack.push(2);std::cout << "iStack.top(): " << iStack.top() << "\n";// manipulate float stack:fStack.push(3.3);std::cout << "fStack.top(): " << fStack.top() << "\n";// assign stack of different type and manipulate againfStack = iStack;fStack.push(4.4);std::cout << "fStack.top(): " << fStack.top() << "\n";// stack for doubless using a vector as an internal containerStack<double, std::vector> vStack;vStack.push(5.5);vStack.push(6.6);std::cout << "vStack.top(): " << vStack.top() << "\n";vStack = fStack;std::cout << "vStack: ";while (!vStack.empty()) {std::cout << vStack.top() << " ";vStack.pop();}std::cout << "\n";return 0;
}
第 6 章 移动语义和 enable_if<>
6.1 完美转发(Perfect Forwarding)
假设希望实现的泛型代码可以将被传递参数的基本特性转发出去:
- 可变对象被转发之后依然可变。
- Const 对象被转发之后依然是 const 的。
- 可移动对象(可以从中窃取资源的对象)被转发之后依然是可移动的。
不使用模板的话,为达到这一目的就需要对以上三种情况分别编程。比如为了将调用f()时传递的参数转发给函数 g():
// test111.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>
#include <windows.h>
using namespace std;#include <utility>
#include <iostream>
class X {};
void g(X&) {std::cout << "g() for variable\n";
}
void g(X const&) {std::cout << "g() for constant\n";
}
void g(X&&) {std::cout << "g() for movable object\n";
}
// let f() forward argument val to g():
void f(X& val) {g(val); // val is non-const lvalue => calls g(X&)
}
void f(X const& val) {g(val); // val is const lvalue => calls g(X const&)
}void f(X&& val) {g(std::move(val)); // val is non-const lvalue => needs ::move()tocall g(X&&)
}
int main() {X v; // create variableX const c; // create constantf(v); // f() for nonconstant object calls f(X&) => calls g(X&)f(c); // f() for constant object calls f(X const&) => calls g(X const&)f(X()); // f() for temporary calls f(X&&) => calls g(X&&)f(std::move(v)); // f() for movable variable calls f(X&&) => callsg(X&&)
}
这里定义了三种不同的 f(),它们分别将其参数转发给 g()
注意其中针对可移动对象(一个右值引用)的代码不同于其它两组代码;它需要用std::move() 来处理其参数,因为参数的移动语义不会被一起传递。虽然第三个 f()中的val 被声明成右值引用,但是当其在 f()内部被使用时,它依然是一个非常量左值(参考附录B),其行为也将和第一个 f()中的情况一样。因此如果不使用 std::move()的话,在第三个f()中调用的将是g(X&) 而不是 g(X&&)。
这个模板只对前两种情况有效,对第三种用于可移动对象的情况无效。基于这一原因,C++11 引入了特殊的规则对参数进行完美转发(perfect forwarding)。实现这一目的的惯用方法如下:
template<typename T>
void f(T&& val) {g(std::forward<T>(val));
}
注意 std::move 没有模板参数,并且会无条件地移动其参数;而 std::forward<>会跟据被传递参数的具体情况决定是否“转发”其潜在的移动语义。
不要以为模板参数 T 的 T&&和具体类型 X 的 X&&是一样的。虽然语法上看上去类似,但是它们适用于不同的规则:
- 具体类型 X 的 X&&声明了一个右值引用参数。只能被绑定到一个可移动对象上(一个prvalue,比如临时对象,一个 xvalue,比如通过 std::move()传递的参数,更多细节参见附录 B)。它的值总是可变的,而且总是可以被“窃取”。
- 模板参数 T 的 T&&声明了一个转发引用(亦称万能引用)。可以被绑定到可变、不可变(比如 const)或者可移动对象上。在函数内部这个参数也可以是可变、不可变或者指向一个可以被窃取内部数据的值。
注意 T 必须是模板参数的名字。只是依赖于模板参数是不可以的。对于模板参数T,形如typename T::iterator&&的声明只是声明了一个右值引用,不是一个转发引用。
因此,一个可以完美转发其参数的程序会像下面这样:
#include <utility>
#include <iostream>
class X {};
void g(X&) {std::cout << "g() for variable\n";
}
void g(X const&) {std::cout << "g() for constant\n";
}
void g(X&&) {std::cout << "g() for movable object\n";
}template<typename T>
void f(T&& val) {g(std::forward<T>(val));
}int main() {X v; // create variableX const c; // create constantf(v); // f() for nonconstant object calls f(X&) => calls g(X&)f(c); // f() for constant object calls f(X const&) => calls g(X const&)f(X()); // f() for temporary calls f(X&&) => calls g(X&&)f(std::move(v)); // f() for movable variable calls f(X&&) => callsg(X&&)
}
6.2 特殊成员函数模板
特殊成员函数也可以是模板,比如构造函数,但是有时候这可能会带来令人意外的结果。
考虑下面这个例子
#include <utility>
#include <string>
#include <iostream>
class Person {private:std::string name;public:// constructor for passed initial name:explicit Person(std::string const& n) : name(n) {std::cout << "copying string-CONSTR for ’" << name << "’\n";}explicit Person(std::string&& n) : name(std::move(n)) {std::cout << "moving string-CONSTR for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls copying string - CONSTRPerson p2("tmp"); // init with string literal => calls movingstring-CONSTRPerson p3(p1); // copy Person => calls COPY-CONSTRPerson p4(std::move(p1)); // move Person => calls MOVE-CONSTreturn 0;
}
例子中 Person 类有一个 string 类型的 name 成员和几个初始化构造函数。为了支持移动语义,重载了接受 std::string 作为参数的构造函数:
现在将上面两个以 std::string 作为参数的构造函数替换为一个泛型的构造函数,它将传入的参数完美转发(perfect forward)给成员 name:
#include <utility>
#include <string>
#include <iostream>
class Person {private:std::string name;public:template<typename T>explicit Person(T&& str) : name(std::forward<T>(str)) {std::cout << "template for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls templatePerson p2("tmp"); // init with string literal => calls template
// Person p3(p1); // build errorPerson p4(std::move(p1)); // move Person => calls MOVE-CONSTreturn 0;
}
问题出在这里:根据 C++重载解析规则(参见 16.2.5 节),对于一个非const 左值的Personp,成员模板
template Person(STR&& n)
通常比预定义的拷贝构造函数更匹配:
Person (Person const& p) 这里 STR 可以直接被替换成 Person&,
但是对拷贝构造函数还要做一步const 转换。额外提供一个非 const 的拷贝
6.3 通过 std::enable_if<>禁用模板
从 C++11 开始,通过 C++标准库提供的辅助模板 std::enable_if<>,可以在某些编译期条件下忽略掉函数模板。
比如,如果函数模板 foo<>的定义如下:
#include <utility>
#include <string>
#include <iostream>template<typename T>
typename std::enable_if < (sizeof(T) > 4) >::type
foo() {
}int main() {foo<double>();// build success//foo<bool>();// build error “std::enable_if<sizeof(T)>4,void>::type foo(void)”的显式 模板 参数无效return 0;
}
这一模板定义会在 sizeof(T) > 4 不成立的时候被忽略掉。如果 sizeof > 4 成立,函数模板会展开成:
template<typename T>
void foo() {
}
也就是说 std::enable_if<>是一种类型萃取(type trait),它会根据一个作为其(第一个)模板参数的编译期表达式决定其行为:
- 如果这个表达式结果为 true,它的 type 成员会返回一个类型:-- 如果没有第二个模板参数,返回类型是 void。 -- 否则,返回类型是其第二个参数的类型。
- 如果表达式结果 false,则其成员类型是未定义的。根据模板的一个叫做SFINAE(substitute failure is not an error,替换失败不是错误,将在 8.4 节进行介绍)的规则,这会导致包含 std::enable_if<>表达式的函数模板被忽略掉。
由于从 C++14 开始所有的模板萃取(type traits)都返回一个类型,因此可以使用一个与之对应的别名模板 std::enable_if_t<>,这样就可以省略掉 template 和::type 了。如下
template<typename T>
std::enable_if_t < (sizeof(T) > 4) >
foo() {
}
如果给 std::enable_if<>或者 std::enable_if_t<>传递第二个模板参数
template<typename T>
std::enable_if_t < (sizeof(T) > 4), T >
foo() {return T();
}
那么在 sizeof(T) > 4 时,enable_if 会被扩展成其第二个模板参数。因此如果与T 对应的模板参数被推断为 MyType,而且其 size 大于 4,那么其等效于
MyType foo()
6.4 使用 enable_if<>
通过使用 enable_if<>可以解决 6.2 节中关于构造函数模板的问题。
我们要解决的问题是:当传递的模板参数的类型不正确的时候(比如不是std::string 或者可以转换成 std::string 的类型),禁用如下构造函数模板:
explicit Person(STR && n): name(std::forward<STR>(n)) {std::cout << "TMPL-CONSTR for ’" << name << "’\n";}
为了这一目的,需要使用另一个标准库的类型萃取,std::is_convertiable。在C++17中,相应的构造函数模板的定义如下:
template<typename STR, typename =
std::enable_if_t<std::is_convertible_v<STR, std::string>>>
Person(STR&& n);
如果 STR 可以转换成 std::string,这个定义会扩展成:
template<typename T,typename = void>Person(STR&& n);
否则这个函数模板会被忽略。
这里同样可以使用别名模板给限制条件定义一个别名:
using EnableIfString =std::enable_if_t<std::is_convertible_v<T, std::string>>;
现在完整 Person 类如下
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>template<typename T>
using EnableIfString =std::enable_if_t<std::is_convertible_v<T, std::string>>;
class Person {private:std::string name;public:// generic constructor for passed initial name:template<typename STR, typename = EnableIfString<STR>>explicit Person(STR && n): name(std::forward<STR>(n)) {std::cout << "TMPL-CONSTR for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls TMPL-CONSTRPerson p2("tmp"); // init with string literal => calls TMPL-CONSTRPerson p3(p1); // OK => calls COPY-CONSTRPerson p4(std::move(p1)); // OK => calls MOVE-CONSTreturn 0;
}
禁用某些成员函数
注意我们不能通过使用 enable_if<>来禁用 copy/move 构造函数以及赋值构造函数。这是因为成员函数模板不会被算作特殊成员函数(依然会生成默认构造函数),而且在需要使用copy 构造函数的地方,相应的成员函数模板会被忽略掉。因此即使像下面这样定义类模板:
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>class C {public:C() = default;template<typename T>C(T const&) {std::cout << "tmpl copy constructor\n";}
};int main() {C x;C y{ x }; // still uses the predefined copy constructor (not the membertemplate)return 0;
}
C y{ x }; 并不会调用模板,调用默认拷贝构造函数
但是也有一个办法:可以定义一个接受 const volatile 的 copy 构造函数并将其标示为delete。这样做就不会再隐式声明一个接受 const 参数的 copy 构造函数。在此基础上,可以定义一个构造函数模板,对于 nonvolatile 的类型,它会优选被选择(相较于已删除的copy 构造函数):
class C {public:C() = default;C(C const volatile&) = delete;// implement copy constructor template with better match:template<typename T>template<typename T>C(T const&) {std::cout << "tmpl copy constructor\n";}
};
这样即使对常规 copy,也会调用模板构造函数:
C x;
C y{x}; // uses the member template
于是就可以给这个模板构造函数添加 enable_if<>限制。比如可以禁止对通过int 类型参数实例化出来的 C<>模板实例进行 copy:
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>template<typename T>
class C {public:C() = default;C(C const volatile&) = delete;// if T is no integral type, provide copy constructor templatewith better match:template < typename = std::enable_if_t < !std::is_integral<T>::value >>C(C<T> const&) {std::cout << "tmpl copy constructor\n";}
};int main() {C<double> x;C y{ x }; // still uses the predefined copy constructor (not the membertemplate)return 0;
}
6.5 使用 concept 简化 enable_if<>表达式
即使使用了模板别名,enable_if 的语法依然显得很蠢,因为它使用了一个变通方法:为了达到目的,使用了一个额外的模板参数,并且通过“滥用”这个参数对模板的使用做了限制。这样的代码不容易读懂,也使模板中剩余的代码不易理解。
原则上我们所需要的只是一个能够对函数施加限制的语言特性,当这一限制不被满足的时候,函数会被忽略掉。
这个语言特性就是人们期盼已久的 concept,可以通过其简单的语法对函数模板施加限制条件。不幸的是,虽然已经讨论了很久,但是 concept 依然没有被纳入C++17 标准。一些编译器目前对 concept 提供了试验性的支持,不过其很有可能在 C++17 之后的标准中得到支持(目前确定将在 C++20 中得到支持)。通过使用 concept 可以写出下面这样的代码
template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) { …
}
6.6 总结
- 在模板中,可以通过使用“转发引用”(亦称“万能引用”,声明方式为模板参数T加&&)和 std::forward<>将模板调用参完美地数转发出去。
- 将完美转发用于成员函数模板时,在 copy 或者 move 对象的时候它们可能比预定义的特殊成员函数更匹配。
- 可以通过使用 std::enable_if<>并在其条件为 false 的时候禁用模板。
- 通过使用 std::enable_if<>,可以避免一些由于构造函数模板或者赋值构造函数模板比隐式产生的特殊构造函数更加匹配而带来的问题。
- 可 以 通 过 删 除 对 const volatile 类 型 参 数 预 定 义 的 特 殊 成 员函数,并结合使用std::enable_if<>,将特殊成员函数模板化。
- 通过 concept 可以使用更直观的语法对函数模板施加限制。