返回
Featured image of post 深入学习C++模板元编程

深入学习C++模板元编程

C++模板元编程学习笔记

Cpp tmeplate

Cpp模板元编程一直被称为C++编程的"黑魔法",本篇博客是笔者学习C++模板元编程的一些笔记,这里不会涉及一些基础的模板类或模板函数的编程方法,而是学习简单的模板的奇技淫巧。

函数模板

这里给出函数模板常见例子:

template<typename T>
T add(T lhs, T rhs){
    return lhs+rhs;
}

int main(){
    int res = add<int>(1,3); // 也可直接add(1,3),编译器会去推导类型

}

两阶段编译检查

模板是分两阶段编译的:

  1. 在模板定义阶段,模板的检查并不包含类型参数的检查。只包含下面几个方面:
  • 语法检查,比如少了分号
  • 使用了未定义的不依赖于模板参数的名称(类型名,函数名,…).
  • 未使用模板参数的static assertions
  1. 在模板实例化阶段,为确保所有代码都是有效的,模板会被再次检查,尤其那些依赖于类型参数的部分。

类型推断

类型推断中的类型转换

在类型推断的时候自动的类型转换是受限制的:

  • 如果调用参数是按引用传递的,任何类型转换都不被允许。通过模板类型参数 T 定义的两个参数,它们实参的类型必须完全一样。

  • 如果调用参数是按值传递的,那么只有退化(decay)这一类简单转换是被允许的: const 和 volatile 限制符会被忽略,引用被转换成被引用的类型,raw array 和函数被转换为相 应的指针类型。通过模板类型参数 T 定义的两个参数,它们实参的类型在退化(decay) 后必须一样。

对默认调用参数的类型推断

需要注意的是,类型推断并不适用于默认调用参数。例如:

template<typename T>
vido foo( T="" );
...
f(1); // 正确,T被推导为int
f(); // Error, 无法推断T的类型

为应对这一情况,你需要给模板类型参数也声明一个默认参数:

template<typename T = std::string>
void foo(T="");
...
f() // ok

多个模板参数

函数返回类型

对于以下情况:

tempalte<typename T, typename U>
T max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
auto res = max(4,7.2);

如果你使用其中一个类型参数的类型作为返回类型,不管是不是和调用者预期地 一样,当应该返回另一个类型的值的时候,返回值会被做类型转换。这将导致返回值的具体 类型和参数的传递顺序有关。如果传递66.66和42给这个函数模板,返回值是double类型 的 66.66,但是如果传递42和66.66,返回值却是int类型的66。

下面我们将逐一讨论应对这一问题的方法:

作为返回类型的模板参数

我们可以为函数返回值单独定义模板参数

tempalte<typename T, typename U, typename RT>
RT max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
// 但是模板类型推断不会考虑返回类型,而 RT 又没有被用作调用参数的类型。因此 RT 不会被
// 推断。这样就必须显式的指明模板参数的类型。比如:
auto res = max<int,double,double>(4,7.2);

-------------------------------------------------

// 当然,你也可以改变模板参数的顺序,让编译器推导形参类型
tempalte<typename RT, typename T, typename U>
RT max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
auto res = max<double>(4,7.2);
返回类型推导

在 C++11 中,尾置返回类型(trailing return type)允许我们使用函数的调用参数。 也就是说,我们可以基于运算符?:的结果声明返回类型:

tempalte<typename T, typename U>
auto max(T lhs, U rhs)-> decltype(lhs>rhs?lhs:rhs){
    return lhs>rhs?lhs:rhs;
}
...
auto res = max(4,7.2);

如果返回类型是由模板参数决定的,那么推断返回类型最简单也是最好的办法就是让编译器 来做这件事。从 C++14 开始,这成为可能,而且不需要把返回类型声明为任何模板参数类型 (不过你需要声明返回类型为 auto):

tempalte<typename T, typename U>
auto max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
auto res = max(4,7.2);
将返回类型声明为公共类型(Common Type)

从 C++11 开始,标准库提供了一种指定“更一般类型”的方式。std::common_type<>::type产生的类型是他的两个模板参数的公共类型。比如:

tempalte<typename T, typename U>
std::common_type_t<T,U> max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
auto res = max(4,7.2);

函数模板的重载

// maximum of two int values:
int max (int a, int b){
    return b < a ? a : b;
}
// maximum of two values of any type:
template<typename T>
T max (T a, T b){
    return b < a ? a : b;
}

int main(){
    ::max(7, 42); // calls the nontemplate for two ints
    ::max(7.0, 42.0); // calls max<double> (by argument deduction)
    ::max(a, b); //calls max<char> (by argument deduction)
    ::max<>(7, 42); // calls max<int> (by argumentdeduction)
    ::max<double>(7, 42); // calls max<double> (no argumentdeduction)
    ::max(a, 42.7); //calls the nontemplate for two ints```
}

一个非模板函数可以和一个与其同名的函数模板共存,并且这个同名的函数模板可以被实例化为与非模板函数具有相同类型的调用参数。在所有其它因素都相同的情况 下,模板解析过程将优先选择非模板函数,而不是从模板实例化出来的函数。

值得注意的是当重载函数模板的时候,你要保证对任意一个调用,都只会有一个模板匹配。如下面这种情况就会发生歧义:

template<typename T, typename U>
auto max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}

template<typename RT, typename T, typename U>
RT max(T lhs, U rhs){
    return lhs>rhs?lhs:rhs;
}
...
max<long double>(1.2,3); // ok,use second tenpalte
max<int>(4,1.2); // Error:both two template match

类模板

友元函数

如果我们试着先声明一个友元函数,然后再去定义它,情况会变的很复杂。事实上我们有 两种选择:

  1. 可以隐式的声明一个新的函数模板,但是必须使用一个不同于类模板的模板参数,比如用U:
template<typename T>
class Stack{
public:
    std::vector<T> elem;
public:
    template<typename U>
    friend std::ostream& operator << (std::ostream & out, Stack<U> const  & value);

};

无论是继续使用 T 还是省略掉模板参数声明,都不可以(要么是里面的T隐藏了外面的T,要么是在命名空间作用域内声明了一个非模板函数)。

  1. 也可以先将 Stack<T>operator<<声明为一个模板,这要求先对 Stack<T>进行声明:
template<typename T>
class Stack;
template<typename T>
std::ostream& operator <<(std::ostream & out, Stack<T> const  & value);

template<typename T>
class Stack{
public:
    std::vector<T> elem;
public:
    friend std::ostream& operator <<  <T>(std::ostream & out, Stack<T> const  & value);
};

类型别名(Type Aliases)

  1. 使用typedef
typedef Stack<int> intStack;
  1. 使用using
using intStack = Stack<int>

别名模板

不同于 typedef, alias declaration 也可以被模板化,这样就可以给一组类型取一个方便的名字。这一特性从 C++11 开始生效,被称作 alias templates。

template<typename T>
using DequeStack = Stack<T,std::deque<T>>;
...
DeaueStack<int>

Alias Templates for Member Types(class 成员的别名模板)

template<typename T>
class MyType{
    using iterator=...
    ...
};

template<typename T>
using MyTypeIterator = typename MyType<T>::iterator;

...
// we can use
MyTypeIterator<int>

Type Traits Suffix_t (Suffix_t 类型萃取)

从 C++14 开始,标准库使用上面的技术,给标准库中所有返回一个类型的 type trait 定义了 快捷方式。比如为了能够使用:

std::add_const_t<T> // since C++14

而不是:

typename std::add_const<T>::type // since C++11

标准库做了如下定义:

namespace std {
template<typename T>
using add_const_t = typename add_const<T>::type;
}

类模板的类型推导

since C++17 如果构造函数能够推断出所有模板参数的类型(对那些没有默认值的模板参数),就不再需要显式的指明模板参数的类型。

推断指引

since C++17

可以通过提供“推断指引”来提供额外的模板参数推断规则,或者修正已有的模板参数推断 规则。比如你可以定义,当传递一个字符串常量或者 C 类型的字符串时,应该用 std::string 实例化 Stack 模板类:

Stack( char const*) -> Stack<std::string>;

这个指引语句必须出现在和模板类的定义相同的作用域或者命名空间内。通常它紧跟着模板 类的定义。->后面的类型被称为推断指引的 guided type

聚合类的模板化

什么是聚合类:聚合类(这样一类 class 或者 struct:没有用户定义的显式的,或者继承而来的构造函数,没 有 private 或者 protected 的非静态成员,没有虚函数,没有 virtual,private 或者 protected 的基类)也可以是模板。

比如:

template<typename T>
struct ValueWithComment {
    T value;
    std::string comment;
};

定义了一个成员 val 的类型被参数化了的聚合类。可以像定义其它类模板的对象一样定义一 个聚合类的对象:

ValueWithComment< int> vc;
vc.value = 42;
vc.comment = "initial value";

从 C++17 开始,对于聚合类的类模板甚至可以使用“类型推断指引”:

ValueWithComment(
char const*, char const*) -> ValueWithComment<std::string>;
ValueWithComment vc2 = {"hello", "initial value"};

没有“推断指引”的话,就不能使用上述初始化方法,因为 ValueWithComment 没有相应的 构造函数来完成相关类型推断。 标准库的std::array<>类也是一个聚合类,其元素类型和尺寸都是被参数化的。C++17 也给它 定义了“推断指引”.

非类型模板参数

非类型模板参数的限制

使用非类型模板参数是有限制的。通常它们只能是整形常量(包含枚举),指向 objects/functions/members 的指针,objects 或者 functions 的左值引用,或者是 std::nullptr_t (类型是 nullptr).

浮点型数值或者 class 类型的对象都不能作为非类型模板参数使用:

当传递对象的指针或者引用作为模板参数时,对象不能是字符串常量,临时变量或者数据成 员以及其它子对象。由于在 C++17 之前,C++版本的每次更都会放宽以上限制,因此还有 一些针对不同版本的限制:

  • 在 C++11 中,对象必须要有外部链接。
  • 在 C++14 中,对象必须是外部链接或者内部链接。

变长模板

大的来了

变长模板实例

#include <initializer_list>
#include <iostream>
#include <vector>
#include <string>

//递归
// template<typename T>
// void print(T value){
//     std::cout<<"this "<<value<<std::endl;
// }
//变长函数模板
// sizeof...()运算符可以扩展成参数包中所包含的参数数目
template<typename T, typename... Args>
void print(T value,Args... args){
    std::cout<<"size if:"<<sizeof...(args)<<std::endl;
    std::cout<<value<<std::endl;
    // since C++17 编译阶段if
    if constexpr(sizeof...(args)>0)
        print(args...);
}

// 初始化列表展开
// lambda表达式
template<typename T, typename... Args>
void print1(T value, Args... args){
    std::cout<<value<<std::endl;
    (void) std::initializer_list<T>{([&args]{
        std::cout<<args<<std::endl;
    }(),value)...};
}

int main(){
    type1<bool> you;
    print1("apple",1,2,3,"sas",12.3);
    print("banan");
}