C++模板编程

模板编程是C++的一个重点和难点,标准库中有非常多的内容都是通过模板实现的(STL)。C++11标准以来,C++在泛型编程方面引入了许多新的特性。本文讨论了围绕C++模板的诸多技术与编程方法。包括traits

各种traits

traits是C++ STL中的重要组成成分

数值部分

  1. 判断是否是整数:std::is_integral<T>::value
  2. 判断是否有符号:std::numeric_limits<T>::is_signed

迭代器部分

判断迭代器种类以及内容:std::iterator_traits。这个trait实现很有意思,为了兼容指针和常指针这两个最原始的“迭代器”,这里使用了SFINAE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
template<class _Iter>
struct _Iterator_traits_base<_Iter, void_t< // 这里的void_t使得不具有下面五个成员的两种指针失配
typename _Iter::iterator_category,
typename _Iter::value_type,
typename _Iter::difference_type,
typename _Iter::pointer,
typename _Iter::reference
> >
{ // defined if _Iter::* types exist
typedef typename _Iter::iterator_category iterator_category;
typedef typename _Iter::value_type value_type;
typedef typename _Iter::difference_type difference_type;

typedef typename _Iter::pointer pointer;
typedef typename _Iter::reference reference;
};

template<class _Iter>
struct iterator_traits
: _Iterator_traits_base<_Iter>
{ // get traits from iterator _Iter, if possible
};

template<class _Ty>
struct iterator_traits<_Ty *>
{ // get traits from pointer
typedef random_access_iterator_tag iterator_category;
typedef _Ty value_type;
typedef ptrdiff_t difference_type;

typedef _Ty *pointer;
typedef _Ty& reference;
};

// 注意 const _Ty *要单独拿出来偏特化,如果用上面的偏特化的话,我们得到的是const _Ty而不是_Ty
template<class _Ty>
struct iterator_traits<const _Ty *>
{ // get traits from const pointer
typedef random_access_iterator_tag iterator_category;
typedef _Ty value_type;
typedef ptrdiff_t difference_type;

typedef const _Ty *pointer;
typedef const _Ty& reference;
};

对象行为部分

C++新标准中使用了is_...able<>函数代替了之前的has_...函数

reference_wrapper

显然std::vector是不能存放T &的,如果不希望放指针进去,可以使用std::reference_wrapper,不过这东西用起来也并不舒服,例如C++不能重载operator.,虽然最近有一些草案正在提到这一点,所以必须得get()一下。
std::reference_wrapper常用于std::bindstd::thread等函数/类上。它们为了保证传入的参数在自己的生命周期中是持续存在的,决定始终通过传值的方式接受参数,即始终拷贝一份,因此此时如果要传递引用,就得使用std::ref包装一下了。

operator重载

operator=operator()operator[]operator->operator T不能作为non-member function,这是因为担心与自动合成的operator=出现行为不一致的问题。其实还可以更深入地考虑这个问题,例如对于比较大小的运算符operator<

1
2
3
4
5
6
7
8
9
10
11
12
struct A;
struct B;
struct A{
operator<(const B & b){
return true;
}
};
struct B{
operator<(const A & b){
return true;
}
};

这显然是不符合逻辑的

const不是编译期常量

C++初始化方式中已经提到常量const是不能在构造函数体中初始化的,但可以在初始化列表中可以进行初始化,对于常量数组或者标准库的std::vector等容器,现在可以使用花括号{}进行初始化。
需要额外说明的是const甚至不能作为模板参数等编译期常量使用。例如在MSVC2015中,下面的代码是无法通过编译的

1
2
3
4
5
6
7
8
9
10
11
12
struct C {
const int x;
C(int _x) :x(_x) {

}
};

int main() {
const C c(1);
int a[c.x];
system("pause");
}

原因是在C.x虽然是常量,但是要到运行期才能知道,这里应该使用的是static const或者constexprconst修饰符实际上的意义更接近于readonly。如果说const能够“节省空间”,那是由于其不可变,所以发生拷贝时,const对象实际上并不发生复制,但只const修饰的类成员仍然是占空间的。

特化using

与函数相同,C++中的using也不能直接特化,如

1
2
3
4
5
template <typename T>
using wrap<T> = T;

template <>
using wrap<int> = int;

必须需要借助类来workaround

1
2
3
4
template <typename T>
struct wrap{
using type = T;
}

函数模板的特化与函数的重载

为了实现函数的“多态”,常有借助模板类偏特化和重载两种方式。

借助重载实现函数的“偏特化”

虽然函数模板不能偏特化,但函数本身就有重载,因此我们可以在每个不同的函数末尾加上一个tag来达到根据某些traits进行“分类讨论“的效果。例如最常用的std::true_typestd::false_type。在STL的实现中使用了很多这样的_tag来控制行为,例如控制std::advance()函数在接受五种不同迭代器时的更新行为,或者原子库中使用tag标记六种内存模型。即使当我们要“偏特化”的是非类型模板参数时,也可以直接利用重载而不是“偏特化”,这时候我们可以在类里面用enum或者static constexpr int将这个值封装起来,也可以直接借助于标准库的std::condition等函数。在下面的例子中,我们需求模板函数的行为根据std::is_same<T, C>::value进行分类讨论。

1
template<typename T, typename U> when_eq(T x, U y); // 1

在没有constexpr if的情况下,用函数重载来做是一种直截了当的方案。但是重载是对类型而言的,而if只能判断true/false值,非类型模板参数又不能使用整型以外的类型。所以必须要有个机制来将整型值包装成类型,这里可以借助标准库提供的std::integral_constant<typename T, T val>,或者也可以自己实现一个Int2Type或者Int2Type2。这个思路可以解决很多问题,例如对成员函数的偏特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <int I>
struct Int2Type
{
enum { value = I };
};
template <int I>
struct Int2Type2
{
static const /* or constexpr */ int value = I;
};

struct X{};
struct Y{};

template <typename T, typename U>
void when_eq(T x, U y, Int2Type<1>){
puts("eq");
}
template <typename T, typename U>
void when_eq(T x, U y, Int2Type<0>){
puts("neq");
}
template <typename T, typename U>
void when_eq(T x, U y){
when_eq(x, y, Int2Type<is_same<T, U>::value>());
}

int main(){
when_eq(X(), Y());
when_eq(X(), X());
}

这里特别提一下enum的性质,根据标准在使用enum E {V1, V2}的值V1V2时是不需要加上E::限定符的。此外其具体类型可以使用std::underlying_type获得。

借助类模板实现偏特化

还可以使用类模板“偏特化”函数,如果需要上下文,那么可以重载operator()的方式实现一个仿函数,如果不需要上下文,可以在里面定义一个static函数。
如果一定需要偏特化,考虑在一个偏特化类中实现static函数。这又带来一个新的问题,考虑要偏特化一个类中成员函数,如果偏特化类,那其他的成员函数也要重复实现一遍,显得很麻烦,对于这个问题,可以参考这里的说明

普通函数、函数模板和全特化模板函数之间的决议

在这一章节中,我们将讨论重载的普通函数、函数模板和其特化函数之间的决议顺序。
模板函数一旦全特化,它就和普通函数一样成为一个strong symbol,我们应当使用inlinestaticextern关键字防止重复定义。
但是对于全特化的函数模板,它和不加template<>的普通函数还是有着很大区别的?根据C++ Premier中的说明

当调用从模板实例化的函数时,只有有限的类型转换可以被应用在模板实参推演过程使用的函数实参上;如果声明一个普通函数则可以考虑用所有的类型转换来转换实参,这是因为普通函数参数的类型是固定的

上面的话是说明template<>特化函数模板比普通函数的匹配要求更高,这是因为普通函数还能接受传入参数的隐式转换。我们在多个函数之间进行决议时总是选择最为精确的函数。

  1. 在这个例子中,aint,显然主模板0能够精确匹配,于是决议到0,输出"template"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // template
    }
  2. 这个例子就是一个经典的SFINAE用法了,我们知道一个数组的大小只能是整数而不能是浮点数,所以当Tdouble时,char(*)[T()]就是ill-formed。根据SFINAE,我们不把它作为错误,而是转而寻求次优解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T, typename = char(*)[T()]>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // function
    }

但我们必须要考虑一个特例,也就是当有多于一个函数能精确匹配的情况,这时候优先级的顺序是普通函数、全特化、主模板

  1. 普通函数先于全特化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }
    // #2
    template<>
    void promotion(double x) {
    puts("template<>");
    }
    int main() {
    promotion(1.0); // function
    }
  2. 全特化版本先于主模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    template<>
    void promotion(double x) {
    puts("template<>");
    }

    int main() {
    promotion(1.0); // template<>
    }
  3. decay

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T & x) {
    puts("template");
    }
    // #1
    void promotion(int x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // function
    }

考虑可访问性

C++中的可访问性包括private、protected和public三类,可访问性是针对而不是对象而言的。
C++中重载决议是在可访问性检查之前进行的

考虑隐式类型转换

在日常编程中,我们我们常常把字符串直接量当做std::string使用,但其实字符串直接量的类型是const char[],这其中涉及到隐式构造函数或者隐式类型转换。这导致在进行重载决议时出现“不合常理”的情况。

普通情况

知乎的这篇问题中,我们看到Test中定义了两个成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public:
void find(const string &first_name, const string &last_name);
void find(const string &name, bool retied);
};

void Test::find(const string &first_name, const string &last_name)
{
cout << "find 1" << endl;
}

void Test::find(const string &name, bool retied)
{
cout << "find 2" << endl;
}

现在考虑下面的调用,我们发现是调用的find 2版本的函数。这是由于const char[]被decay成了const char *,然后隐式转换成了bool

Test().find("a", "b");

隐式类型转换的顺序

类型隐式转换具有下面的顺序:

  1. 精确匹配
    这里对应着四种的decay,即T[] -> T*T& -> TF(...) -> F (*)(...)T const -> T

    1. 无转换
    2. lvalue到rvalue、数组到指针、函数到指针
    3. 限定符转换
      我们可以为任意类型加上CV限定符。对于多重指针来说,前面的重数的限制要高于后面重数的限制,如
      1
      2
      3
      char** p = 0;
      const char** p1 = p; // error: level 2 more cv-qualified but level 1 is not const
      const char* const * p2 = p; // OK: level 2 more cv-qualified and const added at level 1
  2. 类型提升转换(promotion)
    即Numeric promotions,包含Integral promotion和Floating-point promotion
    这里注意,非promotion的整数之间转换都作为conversion,如char -> int

  3. 标准转换
    包含Numeric conversions,如Integral conversions、Floating-point conversions、Floating–integral conversions、Pointer conversions、Boolean conversions
  4. 用户自定义转换
    这就包括了std::string中定义的从const char *的转换了

注意能进行隐式类型转换并不意味着类型相同,所以使用std::is_same进行的判断都是false,例如下面的代码输出都是false。

1
2
std::cout << std::is_same<int, int &>::value << '\n';
std::cout << std::is_same<int, const int &>::value << '\n';

考虑传参时decay的特殊情况

紧接着上面的讨论,我们查看下面的一段代码,这时候输出就变成true了,究其原因是因为

1
2
3
4
5
6
7
8
9
template<typename T, typename U>
bool check(T x, U y){
return std::is_same<T, U>::value;
}
int main(){
int a = 1;
const int & b = a;
std::cout << check(int(), a) << '\n';
}

同样是类型转换问题,在文章中提到了一个特别的情况

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
int compare(const T& a, const T& b)
{
std::cout << "template" << std::endl;
return 0;
}

int compare(const char* a, const char* b)
{
std::cout << "normal" << std::endl;
return 0;
}

现在调用

compare("123456", "123456");

请问是调用了那个函数呢?有意思的是,对于不同编译器,结果还不一样。作者指出对G++/clang来说是调用了模板版本,而我使用VS2015发现调用的是普通版本。
调用模板版本的原因是T = const char [6]相比退化后的const char *更精确。但是调用普通版本的原因是数组传参时要decay成对应的指针。因此对于模板函数和普通函数实际上都不是精确匹配的(都要经过一次类型转换)。根据C++重载决议原则,如果调用具有二义性,则优先选择调用普通函数。
在本篇结尾,作者引用陈硕的观点,认为G++/clang的实现是符合标准的,但是这属于标准的bug。其原因是模板实参推断(template argument deduction)时,除了产生新的实例化之外,编译器只会执行两种转换

  1. const转换:接受const引用或指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化。也就是可以传一个非const到需要const参数的函数。
  2. 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。

由于模板参数使用了数组的const引用类型,所以按照标准应该不将数组向指针进行decay。

考虑继承

SFINAE

在C++中常常出现一些ill-formed的代码,这些代码在函数体或者其他地方出现编译器都是要报错的,SFINAE(Substitution Failure Is Not An Error)规定,当推导模板参数失败(fail)时,编译器不产生错误,而是放弃使用这个重载集推导参数。我们需要注意,这样的替换发生在下面四种情况下:

  1. (Type SFINAE)所有用于函数的类型,包括函数的返回值与参数的类型
  2. (Type SFINAE)所有用于模板参数的类型
  3. (Expression SFINAE)所有用于函数类型中的表达式
  4. (Expression SFINAE)所有用于模板形参声明中的表达式

这些常被使用的ill-formed代码包括下面的形式:

  1. 试图实例化含有多个不同长度包的包展开
  2. 试图创建void的数组、引用的数组、函数的数组、抽象类类型的数组、负大小的数组或零大小的数组
    例如之前提到过的char(*)[0.5],还有char [0]void [3]
  3. 试图在::左侧使用非类或非枚举的类型
    比如int::field是错的
  4. 试图使用类型的不存在的成员
    比如vector<int>::abcdefg是错的,因为它肯定没有这个字段
  5. 试图在错误的地方是用类型的成员(类型、模板、非类型如literal等)

  6. 试图创建指向引用的指针

  7. 试图创建到 void 的引用
  8. 试图创建指向T成员的指针,其中T不是类类型
    这个常常可被用来判断T是否是类类型,代码如下所示。这里C::*表示一个指向类C中某int类型成员的指针,说明了这个指针带有C的上下文,而...相当于一个兜底的东西。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename C> 
    bool test(int C::*){
    return true; // C是类
    }
    template<typename C>
    bool test(...){
    return false;
    }
    struct X{
    double x;
    };

    int main() {
    cout << test<int>(0)<< endl; // false
    cout << test<X>(0)<< endl; // true
    }
  9. 试图将非法类型给予非类型模板参数
    这个SFINAE将在后面std::void_t中被广泛使用

  10. 试图进行非法转换
  11. 试图创建拥有形参类型void的函数
    这个SFINAE对应于前面的“试图将非法类型给予非类型模板参数”
  12. 试图创建返回数组类型或函数类型的函数
  13. 试图创建参数类型或返回类型为抽象类的函数类型

自然而然地想到如果我们需要判定某个实体是否具有某种性质,我们就可以构造一种没有这种特性就会造成ill-formed的表达式或者类型(分别对应Expression SFINAE和Type SFINAE),然后通过函数重载或者模板类的偏特化来进行分类讨论。在使用函数重载时,我们常常在函数最后放一个可能会ill-formed类型的指针。

使用SFINAE判断是否存在成员

SFINAE分为两种,通常见到的是Type SFINAE。关于Expression SFINAE,可以参考这个回答

模板偏特化是相对原型/初等/主(primary template)模板来说的,编译器首先匹配出原型,再根据原型找出合适的特化模板。例如对模板类型参数T而言,T*T&const T &等是它的一个偏特化。通过偏特化我们可以进行分类讨论。

1
2
3
4
5
6
7
8
9
10
11
// 终止条件
template<typename Cont>
bool handle_container(Cont & cont) {
puts("not a container");
};

template<typename Cont>
bool handle_container(Container<Cont> & cont) {
puts("container");
handle_container(cont[0]);
};

偏特化可以用来检查一个类中是否具有某个成员

1
2
3
4
5
6
// https://www.zhihu.com/question/34264462
template<typename Cont, typename = void>
struct has_push_back : std::false_type {};

template<typename Cont>
struct has_push_back<Cont, std::void_t<decltype(&Cont::push_back)>> : std::true_type {};

始终对std::vector<int>断言失败。后来查看SoF,当时以为是MSVC2015对std::void_t的支持有问题。后来在SoF的另一篇博文上发现如果我们的push_back不是成员函数而是数据成员就可以通过。去SoF上问了一波,这是因为std::vector<int>::push_back有多个重载版本,于是应该还要匹配函数签名,因此我们可以写成下面的形式,我们显式构造了vector.push_back(vector::value_type())这个表达式,看它是不是合法。

1
2
3
4
5
6
7
8
9
// https://www.zhihu.com/question/34264462/answer/58260115
template<typename Cont, typename = void>
struct has_push_back : std::false_type{};

template<typename Cont>
using ValueType = typename Cont::value_type;

template<typename Cont>
struct has_push_back<Cont, std::void_t<std::declval<Cont>().push_back(declval<ValueType<Cont>>())>> : std::true_type{};

注意到我们这里都是用类的偏特化来实现的,我也尝试了下使用函数搞一搞

1
2
3
4
5
6
7
8
template<typename Cont, typename = void> 
bool has_push_back() {
return false;
}
template<typename Cont, typename = std::void_t<decltype(&Cont::push_back)>>
bool has_push_back() {
return true;
}

这样会出现函数模板已经定义的错误。在cppreference中给出了说明

A common mistake is to declare two function templates that differ only in their default template arguments. This is illegal because default template arguments are not part of function template’s signature, and declaring two different function templates with the same signature is illegal.

此外在SoF 上给出了如下说明

SFINAE only works for deduced template arguments

判断模板参数是否是广义函数

目前广义函数包括函数、std::function、lambda、仿函数,其中std::is_function只能识别函数。C++17标准添加了std::is_invokable(即之前的std::is_callable),用来表示可以INVOKE的类型。

实现member function的const版本

有些member function的const版本相对于非const版本只是加上了const的限制,重复实现一遍会造成代码的浪费。根据stackoverflow,可以直接const_cast this指针即可。对一个非const加const限制是安全的,但反过来不一定。如果说const函数需要修改非mutable成员,那么可以实现一个static非成员模板函数,将this传进去

推导lambda的类型

根据StackOverflow上的这个答案,lambda是函数对象而不是函数,但是可以被转换(convert to)成std::function,这是由std::function的可以有所有能被调用的类型构造,也就是说这可以这么写std::function<int(int)>lambda = [](int x) {return 0; };。但是类型推导是另一回事,因为lambda并不是std::function,我们只能用auto来声明一个lambda表达式,所以编译器不能通过一个lambda去推导std::function的参数。BTW,Effective Modern C++指出使用auto关键字相对于使用显式声明是比较好的。具体得来说他可以避免default initialization的变量、难以表示的类型、避免可能造成的类型截断问题。
特别地,标准指出一个不捕获lambda表达式可以被转成一个函数指针,但是将这个lambda表达式转成函数指针需要一些技巧,如借助lambda表达式不借助lambda表达式。特别地,函数指针可以看做函数的入口点,它是全局的,而一个函数对象如std::function可以携带context,所以是不能转换为一个函数指针的。在这里还需要将函数签名与函数指针区分开来,一般在使用函数签名的场合常需要考虑C linkage。
因此比较好的解决方式是直接使用template<typename F>,所以说现在的问题是如何得到F的返回值类型。
这时候可以借助std::result_of来推导typename F的返回类型,注意这里要写成decltype(lambda_func)(),而不是decltype(lambda_func)

1
2
3
4
auto lambda_func = [](){
return 1 + 2;
};
using lambda_func_t = std::result_of<decltype(lambda_func)()>::type;

此外,经测试可以推导函数对象的返回类型,不能推导C原生函数的返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int native_func(int a) {
return 0;
}
struct object_func {
int operator()(int a) {
return 0;
}
};
template<typename F>
auto call_func(F f)-> std::result_of_t<F(int)> {
return 0;
}
int main() {
typename std::result_of<object_func(int)>::type x; // Yes
typename std::result_of<native_func(int)>::type y; // No
object_func of;
call_func(of); // Yes
call_func(native_func); // Yes
system("pause");
}

参考了StackOverflow上的答案,这是因为原生函数并不是一个type,但是std::result_of<F(Args...)>中的F必须要是一个类型,合适的解决方法是直接使用decltype+declvaldecltype(native_func(std::declval<int>())),而不是用std::result_of。此外,在C++17标准之后,可以使用std::invoke_result来替代std::result_of
此外,从这个回答可以看到,可以实现一个function_traits。这个类型类似pattern matching,能够同时处理函数对象和函数的指针的情况,并且能够推导出参数和返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())> {};

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
{
enum { arity = sizeof...(Args) };
typedef ReturnType result_type;
template <size_t i>
struct arg
{
typedef typename std::tuple_element<i, std::tuple<Args...>>::type type;
};
};

在类模板中声明友元函数模板

在一个模板类farray中声明一个友元函数

1
2
3
4
5
6
7
8
9
template<typename T>
struct farray{
// ...
friend farray<bool> operator<(const farray<T> & x, const T & y);
}
template <typename T>
farray<bool> operator<(const farray<T> & x, const T & y){
// ...
}

可能会报error LNK2019: 无法解析的外部符号 “struct for90std::farray __cdecl for90std::operator<(struct for90std::farray const &,int const &)” (??Mfor90std@@YA?AU?$farray@_N@0@AEBU?$farray@H@0@AEBH@Z)错误
原因是T是类模板farray的模板参数,随着类模板特化。所以应该给友元函数operator<独立的模板参数,改成这样就好了

1
template<typename U> friend farray<bool> operator<(const farray<U> & x, const U & y);

详细可以参考这篇答案

通用引用

根据Effective Modern C++ Item 24的规定。函数签名中的T&&,如果T是需要推导得来的,这样的T表示通用引用(universal/forward reference)。通用引用是C++通过引用折叠(reference collapsing)表现出的一个特性,一个通用引用可以绑定到任何由cv修饰的引用上。
容易混淆的通用/右值引用包括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 以下属于rvalue reference
void f(Widget&& param);

Widget&& var1 = Widget();

// 通用引用定义必须是T&&或者auto&&的形式
template<typename T>
void f(std::vector<T>&& param)

class vector<T, allocator<T>>{
public:
// 通用引用中必须发生类型推导
void push_back(T&& x);
};

// 以下属于universal reference
auto&& var2 = var1;

template<typename T>
void f(T&& param);

如果对于参数包不加上通用引用Args&&,那这个参数包就不能接受一个左值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T, typename ... Args>
void test_pack_lvalue(T & x, Args ... args) {
x = 1;
test_pack(forward<Args>(args)...);
}
template <typename T, typename ... Args>
void test_pack_lvalue(T & x) {
x = 1;
}
template <typename T, typename ... Args>
void test_pack_clvalue(const T & x, Args ... args) {

test_pack_clvalue(forward<Args>(args)...);
}
template <typename T, typename ... Args>
void test_pack_clvalue(const T & x) {

}
int main()
{
int a = 0, b = 0, c = 0;
test_pack_clvalue(a, b, c);
}

常用术语

参数包:parameter pack
类型限定符(const, volatile):qualification adjustment
返回类型后置:trailing return type
函数签名:signature
模板类型推导:template argument deduction
聚合体Aggregates和POD(Plain Old Data)=trival+standard layout:见此解释
非类型模板参数:non type template parameter
包扩展:pack expansion
花括号初始化器:brace-init-list
省略号(…):ellipsis
符号扩展、零扩展:sign/zero extension
可变参数模板:Variadic Templates pack
展开模式:pattern
决议:resolution