模板与泛型(二)


模板实参的推断和引用

函数模板的形参可以为左值引用或者是右值引用,如果左值引用,又可分为普通的T&const T&,其参数推断规则如下

左值引用参数推断

当一个函数参数是一个普通的左值引用时T&,其传递的实参只能是一个变量或者是一个返回引用类型的表达式。实参可以是const类型也可以不是,如果是const的,则T被推断为const类型

template<typename T>
void f1(T& op); //实参必须是一个左值
f1(i); //i是一个int;T是int
f1(ci); //ci是一个const int,则T是const int
f1(5); //错误,5是一个右值

如果模板参数是const T&,则它可以接受任何类型的实参(const或非const的对象,临时变量和字面常量)。当实参是const类型时,T不会被推断为const类型,因为const已经是模板参数类型的一部分了

template<typename T>
void f2(const T& op); //可以接受右值
//f2的模板参数是const T&, 实参的const是无效的
//在下面每个调用中,f2的参数都被推断为const int&
f1(i); //i是一个int;T是int
f1(ci); //ci是一个const int,则T是int
f1(5); //const T&可以绑定右值,T是int

右值引用参数推断和引用折叠

同理当一个模板参数声明为右值引用时,它将接受一个右值作为实参

template<typename T>
void f3(T&&);
f3(42); //实参是一个int型右值,T是int

C++编译器对T&&做了一些特殊的设定,具体来说有两点,第一点是如果实参是一个左值,按照上面定义,它是不能直接绑定到右值引用(T&&)上的,但实际上却可以。假定i是一个int对象,当我们调用f3(i)时,编译器会推断T的类型为int&,而非int,此时f3接受的参数变成了一个左值引用的右值引用。通常,我们是不能定义一个引用的引用的,但是编译器为我们提供了第二条特殊设定,即如果我们间接创建了一个引用的引用,那么这些引用形成了折叠。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用。在新标准中,折叠也可产生一个右值引用,这种情况只能发生在右值引用的引用。即,对于一个给定类型X:

  • X& &, X& &&X&& &都折叠成X&
  • X&& &&折叠成X&&

这两个规则导致下面几个的结论,对于template<typename T> void f3(T&&)这样的模板

  • 如果传递的参数是左值,T推导的类型为T&,而T& &&会被折叠为T&,因此参数的实际类型为T&
  • 如果传递的参数时左值引用,情况和上面一致
  • 如果传递的参数是右值,T推导的类型为T,因此参数的实际类型为 T&&
  • 如果传递的参数是右值引用,T推导的类型为 T&&,而T&& &&折叠成了T&&,因此参数的实际类型为T&&

T&& 的作用主要是保持值类别进行转发,它有个名字就叫“转发引用”(forwarding reference)。因为既可以是左值引用,也可以是右值引用,它也曾经被叫做“万能引用”(universal reference)。

理解std::move

我们可以通过std::move将一个左值绑定要一个右值引用上。由于move本质上可以接受任何类型的实参,因此推断它的实参为T&&

template<class T>
constexpr remove_reference_t<T>::type&& move(T&& t) noexcept {
    return static_cast<typename remove_reference_t<T>::type&&>(t)
}

通过引用折叠,move即可以接受左值,也可以接受右值。我们分析两个例子

string s1("hi!");
string s2 = std::move(string("bye!"));

对于s1

  • 类型推断Tstring
  • remove_referencestring进行实例化,remove_reference<string>::typestring
  • move返回值类型为string&&move的参数类型为string&&

因此,move被实例化为

string&& move(string&& t) {
    return static_cast<string&&>(t);
}

由于t自身就是string&&,因此static_cast什么都不做,直接返回t

对于s2std::move接收的是一个左值引用string&

  • 类型推断Tstring&
  • remove_referencestring&进行实例化,remove_reference<string&>::typestring
  • move返回值类型仍为string&&
  • move的参数t实例化为string& &&,根据上面的规则,会折叠为string&

因此,move被实例化为

string&& move(string& t) {
    return static_cast<string&&>(t);
}

由于此时tstring&,因此static_cast会将其cast成string&&。实际上,C++允许使用static_cast来转换一个左值到右值,例如下面代码是合法的

int a = 10;
int&& b = static_cast<int&&>(a);

转发

某些函数需要将一个或者多个实参类型不变的转发给其它函数,不论是否是const,左值还是右值。C++提供了一个std::forward的函数来转发模板类型的参数,std::forward可以保持给定实参的左值/右值属性

template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

使用std::forward有两点需要注意

  1. 传递的参数通常为T&&,即可以绑定到任意参数
  2. std::move不同,std::forward<T>必须通过显式模板实参来调用

可变参数模板

可变参数模板接受任意多个模板参数和函数参数,用typename...或者classname...表示。例如

// Args表示一个或多个模板参数
// rest表示一个或多个是函数参数
template<typename T, typename... Args>
void foo(const T& t, const Args& ... rest);

编译器会根据实参实例化出不同的模板,例如

int i=0; double d = 3.14; string s = "hi";
foo(i, s, 42, d);
foo(s, 32, "hi");
foo(d, s);
foo("hi");

编译器会为foo实例化出四个不同版本

void foo(cosnt int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[2]&);
void foo(const double& const int&);
void foo(const char[2]&);

当我们既不知道函数的参数个数,也不知道它们的类型时,可变参数模板是很有用的。另一个例子是可以用可变参数模板来处理递归

template<typename T>
ostream &print(ostream &os, const T& t) {
    return os<<t; //print t
}

template<typename T, typename...Args>
ostream &print(ostream &os, const T& t, const Args&... rest) {
    os<< t << ",";
    return print(os, rest...); //递归
}

print(cout, i, s, 42);

此时前两次调用会走可变参数函数,最后一次调用走第一个普通模板函数

sizeof...

sizeof...返回一个常量表达式,可以用来知道模板和函数有多少个参数

template<typename ... Args>
void g(Args ... args) {
    std::cout<<sizeof...(Args)<<std::endl;
    std::cout<<sizeof...(args)<<std::endl;
}

std::forward

我们可以用std::forward将可变参数原封不动的传给其它函数

class StrVec {
public:
    temaplate<class ... Args>
    void emplace_back(Args&&...) {
        // some code
        alloc.construct(first_free++, std::forward<Args>(args)...);
    }
}

前面提到过,std::forward接受的参数通常是T&&,对于可变参数也是如此。上面的emplace_back接受一个string变量,由于string有多个构造函数,且参数都不相同,因此emplace_back接受的是可变参数,例如

svec.emplace_back(10, 'c'); // add cccccccccc to the end of the array

编译器会产生下面参数传给cosntruct

std::forward<int>(10), std::forward<char>(c)

类似的,如果传入一个右值,svec.emplace_back("hi");,它将以如下形式传递 std::forward<string>(string("hi"))

类模板的特例化

前面文章中提到函数模板可以特例化,这里我们继续讨论类模板的特例化,一个好的例子是hash模板,因为不同的类的hash运算不一样,因此我们需要为每个类实现一个特例化的hash函数。一个特例化的hash类必须定义

  1. 一个重载的调用运算符operator()返回类型为size_t,参数类型为T
  2. 定义两个类型成员(type members)作为operator()
  3. 定义默认构造函数和拷贝赋值运算

我们需要在namespace std下面特例化这个模板

namespace std {
template<>
struct hash<Sales_data>{
    typedef size_t result_type;
    typedef Sales_data argument_type;
    size_t operator()(const Sales_data &) const;
};
size_t hash<Sales_data>::operator()(const Sales_data &) const {
    return hash<string>()(s.bookNo) ^
           hash<string>()(s.units_sold) ^
           hash<string>()(s.revenue);
 }
}

由于hash<Sale_data>使用Sale_data的私有成员,我们必须将其声明为Sales_data的友元

class Sale_data {
//...    
template<class T> class std::hash;
friend class std::hash<Sales_data>
//...
};

与函数模板不同,类模板的参数可以部分特例化,例如remove_reference的定义如下

template<class T> struct remove_reference {
    typedef T type;
};
// partially specialized for lvalue and rvalue reference
template<class T> struct remove_reference<T&> {
    typedef T type;
};
template<class T> struct remove_reference<T&&> {
    typedef T type;
}

部分特例化的模板参数必须是原模板参数的一个子集,上面例子中T&T&&都是T的一个特例。下面例子中我们定义一个int型的a

//decltype(42) is int, use the original template
remove_reference<decltype(42)>::type a;

int i;
//decltype(i) is int&, use the T& template
remove_reference<decltype(i)>::type a;
//decltype(std::move(i)) is int&&, use the T&& template
remove_reference<decltype(std::move(i))>::type a;

我们还可以特例化类的成员函数

template<typename T>
struct Foo {
    Foo(const T& = T()):mem(t) {}
    void Bar() { ... }
    T mem;
};
template<>
void Foo<int>::Bar() {
    //Foo<int> is specialized for int
}

//main.cpp
Foo<int> fi;
fi.Bar() ; //this will call the specilized function

Resources