C++ template-2

 第 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 coll
    typename T::const_iterator end(coll.end()); // end position

    for (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 value
    int* 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 value
    int* 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 使用裸数组或者字符串常量的模板

        当向模板传递裸数组或者字符串常量时,需要格外注意以下内容:
        第一,如果参数是按引用传递的,那么参数类型不会退化(decay )。也就是说当传递 ”hello”
作为参数时,模板类型会被推断为 char const[6] 。这样当向模板传递长度不同的裸数组或者
字符串常量时就可能遇到问题,因为它们对应的模板类型不一样。只有当按值传递参数时,
模板类型才会退化( decay ),这样字符串常量会被推断为 char const * 。相关内容会在第 7
章进行讨论。

5.5 成员模板

        类的成员也可以是模板,对嵌套类和成员函数都是这样。这一功能的作用和优点同样可以通
Stack<> 类模板得到展现。通常只有当两个 stack 类型相同的时候才可以相互赋值( stack
的类型相同说明它们的元素类型也相同)。即使两个 stack 的元素类型之间可以隐式转换,
也不能相互赋值:
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
        默认的赋值运算符要求等号两边的对象类型必须相同,因此如果两个 stack 之间的元素类型
不同的话,这一条件将得不到满足。
        但是,只要将赋值运算符定义成模板,就可以将两个元素类型可以做转换的 stack 相互赋值。
新的 Stack<> 定义如下:
template<typename T>
class Stack {
private:
    std::deque<T> elems; // elements
public:
    void push(T const&); // push element
    void pop(); // pop element
    T const& top() const; // return top element
    bool empty() const { // return whether the stack is empty
        return elems.empty();
    }
    // assign stack of elements of type T2
    template<typename T2>
    Stack& operator= (Stack<T2> const&);
};
以上代码中有如下两点改动:
1. 赋值运算符的参数是一个元素类型为 T2 stack
2. 新的模板使用 std::deque<> 作为内部容器。这是为了方便新的赋值运算符的定义。
新的赋值运算符被定义成下面这样:
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 stack
    elems.clear(); // remove existing elements
    while (!tmp.empty()) { // copy all elements
        elems.push_front(tmp.top());
        tmp.pop();
    }
    return *this;
}
下面先来看一下成员模板的定义语法。在模板类型为 T 的模板内部,定义了一个模板类型为
T2 的内部模板
        在模板函数内部,你可能希望简化 op2 中相关元素的访问。但是由于 op2 属于另一种类型
(如果用来实例化类模板的参数类型不同,那么实例化出来的类的类型也不同),因此最好
使用它们的公共接口。这样访问元素的唯一方法就是通过调用 top() 。这就要求 op2 中所有
元素相继出现在栈顶,为了不去改动 op2 ,就需要做一次 op2 的拷贝。由于 top() 返回的是
最后一个被添加进 stack 的元素,因此需要选用一个支持在另一端插入元素的容器,这就是
为什么选用 std::deque<> 的原因,因为它的 push_front() 方法可以将元素添加到另一端。
为了访问 op2 的私有成员,可以将其它所有类型的 stack 模板的实例都定义成友元:

 

        当然,这样的赋值就不会改变 floatStack 的类型,也不会改变它的元素的类型。在赋值之后, floatStack 存储的元素依然是 float 类型, top() 返回的值也依然是 float 类型。
        看上去这个赋值运算符模板不会进行类型检查,这样就可以在存储任意类型的两个 stack
间相互赋值,但是事实不是这样。必要的类型检查会在将源 stack (上文中的 op2 或者其备
temp )中的元素插入到目标 stack 中的时候进行: elems . push_front ( tmp . top ());
        比如如果将存储 string stack 赋值给存储 int stack ,那么在编译这一行代码的时候会遇
到如下错误信息:不能将通过 tmp.top() 返回的 string 用作 elems.push_front() 的参数(不同编
译器产生的错误信息可能会有所不同,但大体上都是这个意思):

成员模板的特例化

 成员函数模板也可以被全部或者部分地特例化。比如对下面这个例子:

// 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 hello
    std::cout << s1.get<bool>() << "\n"; //prints false
    BoolString s2("on");
    std::cout << s2.get<bool>() << "\n"; //prints true
    return 0;
}

其成员函数模板 get()进行全特例化:bool get<bool>()
        注意我们不需要也不能够对特例化的版本进行声明;只能定义它们。由于这是一个定义于头
文件中的全实例化版本,如果有多个编译单 include 了这个头文件,为避免重复定义的错误,
必须将它定义成 inline 的。
        

特殊成员函数的模板

        如果能够通过特殊成员函数 copy 或者 move 对象,那么相应的特殊成员函数( copy 构造函
数以及 move 构造函数)也将可以被模板化。和前面定义的赋值运算符类似,构造函数也可
以是模板。但是需要注意的是,构造函数模板或者赋值运算符模板不会取代预定义的构造函
数和赋值运算符。成员函数模板不会被算作用来 copy 或者 move 对象的特殊成员函数。在
上面的例子中,如果在相同类型的 stack 之间相互赋值,调用的依然是默认赋值运算符。
这种行为既有好处也有坏处:
(1)某些情况下,对于某些调用,构造函数模板或者赋值运算符模板可能比预定义的
copy/move 构造函数或者赋值运算符更匹配,虽然这些特殊成员函数模板可能原本只打
算用于在不同类型的 stack 之间做初始化。详情请参见 6.2 节。
(2)想要对 copy/move 构造函数进行模板化并不是一件容易的事情,比如该如何限制其存
在的场景。详情请参见 6.4 节。

template 的使用

        某些情况下,在调用成员模板的时候需要显式地指定其模板参数的类型 。这时候就需要使用
关键字 template 来确保符号 < 会被理解为模板参数列表的开始,而不是一个比较运算符。考
虑下面这个使用了标准库中的 bitset 的例子:
#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>>();
}
        对于 bitset 类型的 bs ,调用了其成员函数模板 to_string() ,并且指定了 to_string() 模板的所有
模板参数。如果没有 .template 的话,编译器会将 to_string() 后面的 < 符号理解成小于运算符,
而不是模板的参数列表的开始。 这一这种情况只有在点号前面的对象依赖于模板参数的时候
才会发生。在我们的例子中, bs 依赖于模板参数 N
.template 标识符(标识符 ->template ::template 也类似)只能被用于模板内部,并且它前
面的对象应该依赖于模板参数。详情请参见 13.3.3

泛型 lambdas 和成员模板

在 C++14 中引入的泛型 lambdas,是一种成员模板的简化。对于一个简单的计算两个任意类
型参数之和的 lambda:
[] (auto x, auto y) {
    return x + y;
}
编译器会默认为它构造下面这样一个类:
class SomeCompilerSpecificName {
public:
    SomeCompilerSpecificName(); // constructor only callable by compiler
    template<typename T1, typename T2>
    auto operator() (T1 x, T2 y) const {
        return x + y;
    }
};

5.6 变量模板

C++14 开始,变量也可以被某种类型参数化。称为变量模板。
例如可以通过下面的代码定义 pi ,但是参数化了其类型:
        template < typename T >
        constexpr T pi { 3.1415926535897932385 };
注意,和其它几种模板类似,这个定义最好不要出现在函数内部或者块作用域内部。
        在使用变量模板的时候,必须指明它的类型。比如下面的代码在定义 pi<> 的作用域内使用了
两个不同的变量:
        std :: cout << pi < double > << \n ;
        std :: cout << pi < float > << \n ;

用于数据成员的变量模板

        变量模板的一种应用场景是,用于定义代表类模板成员的变量模板。比如如果像下面这样定
义一个类模板:
template<typename T>
class MyClass {
public:
    static constexpr int max = 1000;
};
那么就可以为 MyClass<> 的不同特例化版本定义不同的值:
        template < typename T >
        int myMax = MyClass < T >:: max ;
应用工程师就可以使用下面这样的代码:
        auto i = myMax < std :: string >;
而不是:
        auto i = MyClass < std :: string >:: max ;
这意味着对于一个标准库的类:
namespace std {
template<typename T>
class numeric_limits {
    public: 
        …
        static constexpr bool is_signed = false; …
};
}
可以定义:
template < typename T >
constexpr bool isSigned = std :: numeric_limits < T >:: is_signed ;
这样就可以用:
isSigned < char >
代替:
std :: numeric_limits < char >:: is_signed

类型萃取 Suffix_v

C++17 开始,标准库用变量模板为其用来产生一个值(布尔型)的类型萃取定义了简化方
式。比如为了能够使用:
std :: is_const_v < T > // since C++17
而不是:
std :: is_const < T >:: value //since C++11
标准库做了如下定义:
namespace std {
        template < typename T >
               constexpr bool is_const_v = is_const < T >:: value ;
}

5.7 模板参数模板

        如果允许模板参数也是一个类模板的话,会有不少好处。在这里依然使用 Stack 类模板作为
例子。
        对 5.5 节中的 stack 模板,如果不想使用默认的内部容器类型 std::deque ,那么就需要两次指
stack 元素的类型。也就是说为了指定内部容器的类型,必须同时指出容器的类型和元素
的类型:
        Stack < int , std :: vector < int >> vStack ; // integer stack that uses a vector
        使用模板参数模板,在声明 Stack 类模板的时候就可以只指定容器的类型而不去指定容器中
元素的类型:
        Stack < int , std :: vector > vStack ; // integer stack that uses a vector
        为此就需要在 Stack 的定义中将第二个模板参数声明为模板参数模板。可能像下面这样:

#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 element
    void pop(); // pop element
    T const& top() const; // return top element
    bool empty() const { // return whether the stack is empty
        return elems.empty();
    } …
};
区别在于第二个模板参数被定义为一个类模板:
        template < typename Elem > class Cont
默认类型也从 std::deque<T> 变成 std::deque 。这个参数必须是一个类模板,它将被第一个模
板参数实例化:
        Cont < T > elems ;
        用第一个模板参数实例化第二个模板参数的情况是由 Stack 自身的情况决定的。实际上,可
以在类模板内部用任意类型实例化一个模板参数模板。
        和往常一样,声明模板参数时可以使用 class 代替 typename 。在 C++11 之前, Cont 只能被某 个类模板的名字取代。
        从 C++11 开始,也可以用别名模板( alias template )取代 Cont ,但是直到 C++17 ,在声明模 板参数模板时才可以用 typename 代替 class:        

        这两个变化的目的都一样:用 class 代替 typename 不会妨碍我们使用别名模板( alias
template )作为和 Cont 对应的模板参数。
        由于模板参数模板中的模板参数没有被用到,作为惯例可以省略它(除非它对文档编写有帮
助):
成员函数也要做相应的更改。必须将第二个模板参数指定为模板参数模板。比如对于 push()
成员,其实现如下:
template < typename T , template < typename > class Cont >
void Stack < T , Cont >:: push ( T const & elem )
{
        elems . push_back ( elem ); // append copy of passed elem
}
注意,虽然模板参数模板是类或者别名类( alias templates )的占位符,但是并没有与其对
应的函数模板或者变量模板的占位符

 模板参数模板的参数匹配

如果你尝试使用新版本的 Stack ,可能会遇到错误说默认的 std::deque 和模板参数模板 Cont
不匹配。这是因为在 C++17 之前, template<typename Elem> typename Cont = std::deque
的模板参数必须和实际参数( std::deque )的模板参数匹配(对变参模板有些例外,见 12.3.4
节)。而且实际参数( std::deque 有两个参数,第二个是默认参数 allocator )的默认参数也
要被匹配,这样 template<typename Elem> typename Cont = std::dequ 就不满足以上要求(不
过对 C++17 可以)。
作为变通,可以将类模板定义成下面这样:
template<typename T, template<typename Elem,
typename Alloc = std::allocator<Elem>> class Cont = std::deque>
class Stack {
private:
        Cont<T> elems; // elements
…
};
其中的 Alloc 同样可以被省略掉。       
因此最终的 Stack 模板会像下面这样(包含了赋值运算符模板
#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; // elements
	public:
		void push(T const&); // push element
		void pop(); // pop element
		T const& top() const; // return top element
		bool empty() const { // return whether the stack is empty
			return elems.empty();
		}
		// assign stack of elements of type T2
		template<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 elements
	elems.insert(elems.begin(), // insert at the beginning
		op2.elems.begin(), // all elements from op2
		op2.elems.end());
	return *this;
}

int main()
{
	Stack<int> iStack; // stack of ints
	Stack<float> fStack; // stack of floats
	// manipulate int stack
	iStack.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 again
	fStack = iStack;
	fStack.push(4.4);
	std::cout << "fStack.top(): " << fStack.top() << "\n";
	// stack for doubless using a vector as an internal container
	Stack<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 variable
  X const c; // create constant
  f(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 variable
  X const c; // create constant
  f(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 - CONSTR
  Person p2("tmp"); // init with string literal => calls movingstring-CONSTR
  Person p3(p1); // copy Person => calls COPY-CONSTR
  Person p4(std::move(p1)); // move Person => calls MOVE-CONST
  return 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 template
  Person p2("tmp"); // init with string literal => calls template
// Person p3(p1); // build error
  Person p4(std::move(p1)); // move Person => calls MOVE-CONST
  return 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-CONSTR
  Person p2("tmp"); // init with string literal => calls TMPL-CONSTR
  Person p3(p1); // OK => calls COPY-CONSTR
  Person p4(std::move(p1)); // OK => calls MOVE-CONST


  return 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 总结 

  1. 在模板中,可以通过使用“转发引用”(亦称“万能引用”,声明方式为模板参数T加&&)和 std::forward<>将模板调用参完美地数转发出去。
  2. 将完美转发用于成员函数模板时,在 copy 或者 move 对象的时候它们可能比预定义的特殊成员函数更匹配。
  3. 可以通过使用 std::enable_if<>并在其条件为 false 的时候禁用模板。
  4. 通过使用 std::enable_if<>,可以避免一些由于构造函数模板或者赋值构造函数模板比隐式产生的特殊构造函数更加匹配而带来的问题。
  5. 可 以 通 过 删 除 对 const volatile 类 型 参 数 预 定 义 的 特 殊 成 员函数,并结合使用std::enable_if<>,将特殊成员函数模板化。
  6. 通过 concept 可以使用更直观的语法对函数模板施加限制。

 

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值