C++17类模板参数推导

Share on:
400 Words | Read in about 2 Min | View times

Overview

在C++17之前,没有默认值的类模板参数总是需要显式将所有实参传递给类模板,而从C++17开始对于显示指定模板实参的限制已经取消。换言之,只要构造函数能推导出所有没有默认值的模板参数时,就可以省略模板实参的定义。本节内容将详细介绍C++17利用构造函数进行模板参数推导的过程。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

引子

我们以下面这个模板类作为引入:

1template<typename T>
2class Stack {
3public:
4    Stack() = default; //默认构造函数,由编译器自动生成
5    Stack(const Stack& s) = default; //拷贝构造函数
6    //...
7private:
8    std::vector<T> m_cont;
9};

在C++17以前,我们面对这个模板类进行实例化时,必须显式指定模板参数类型:

1//C++17之前
2Stack<int> s1;
3Stack<int> s2 = s1; //拷贝构造
4
5//C++17之后
6Stack s3 = s1; //拷贝构造,省略模板实参

从C++17开始,模板类进行实例化时,对于没有默认值的模板参数,只要构造函数能推导出模板类型,那么模板实参的定义就可以省略。

构造函数模板参数类型推导

类模板参数推导,Class Template Argument Deduction,简称CTAD,是指通过实例化时构造函数的参数来推导出整个类模板的模板参数类型。

我们在这个模板类中再增加一个构造函数,给定一些初始元素的参数,就可以支持类型的推导:

 1template<typename T>
 2class Stack {
 3public:
 4    Stack() = default; //默认构造函数,由编译器自动生成
 5    Stack(const Stack& s) = default; //拷贝构造函数
 6    Stack(const T& elem) //通过一个元素初始化容器
 7        : m_cont({elem}) //初始化列表
 8    {
 9        std::cout << typeid(T).name() << std::endl;
10    }
11    //...
12private:
13    std::vector<T> m_cont;
14};

这样我们就可以通过下面的定义语句来初始化一个栈对象:

1//C++17
2Stack s = 0; //推导为Stack<int>

通过使用整数0来初始化,调用的是第3个构造函数,模板参数类型T就会被推导为int,这样就实例化了一个Stack<int>

注意此时的default构造函数是不可以省略的,因为一旦定义了自定义的构造函数(第3个),编译器就不会自动生成默认构造函数了,所以需要显式定义default构造函数。

由于vector没有直接接收单个参数的构造函数,所以这里需要使用初始化列表来初始化m_cont

字符串字面量的CTAD

还是上面这个例子,我们还可以使用字符串字面量来初始化栈对象:

 1//C++17
 2template<typename T>
 3class Stack {
 4public:
 5    Stack() = default;
 6    Stack(const Stack& s) = default;
 7    Stack(const T& elem)
 8        //: m_cont({elem}) //为了演示字符串字面量的推导暂时注释掉
 9    {
10        std::cout << typeid(T).name() << std::endl;
11    }
12    //...
13private:
14    std::vector<T> m_cont;
15};
16
17int main() {
18    Stack s{"zhxilin"}; //A8_c,即const char[8]
19    return 0;
20}

上面这个例子会推导为Stack<const char[8]>。这似乎与我们的预期不一致,我们预期是能推导为原始指针类型,而不是原始数组类型,即推导为Stack<const char*>而不是Stack<const char[8]>

原因是这个例子调用的是构造函数Stack<const T& elem>,通过引用传递模板类型T实参,该实参的类型不会进行退化(decay)。退化是指原始数组类型转换成相应的原始指针类型的机制。字符串字面量是数组类型const char[],当使用引用传递数组时,会推导为数组引用,而不会退化为指针const char*

解决这个问题的办法是把引用传参改为按值传参。按值传递模板类型T的实参时,该实参类型会退化:

 1template<typename T>
 2class Stack {
 3public:
 4    Stack() = default;
 5    Stack(const Stack& s) = default;
 6    Stack(T elem) //按值传递
 7        : m_cont({elem})
 8    {
 9        std::cout << typeid(T).name() << std::endl;
10    }
11    //...
12private:
13    std::vector<T> m_cont;
14};
15
16int main() {
17    Stack s{"zhxilin"}; //PKc,即const char*
18    return 0;
19}

这样改造之后,s的类型将推导为Stack<const char*>了。

上述改造后的构造函数还会存在拷贝的情况,我们可以进一步优化,使用移动语义:

 1template<typename T>
 2class Stack {
 3public:
 4    Stack() = default;
 5    Stack(const Stack& s) = default;
 6    Stack(T elem)
 7        : m_cont({std::move(elem)})
 8    {
 9        std::cout << typeid(T).name() << std::endl;
10    }
11    //...
12private:
13    std::vector<T> m_cont;
14};

推导指引

上面这个问题除了定义按值传递的构造函数外,还可以通过推导指引(Deduction Guides)的方式,定向将一些类型推导成目标类型。我们可以定义一个推导指引,禁止自动推导为原始指针const char*,而是指向性地推导为std::string,这样只要传递的模板参数类型是字符串字面量或C风格字符串,就都会推导为std::string类型。

推导指引的语法是带尾随返回类型的函数声明的语法,但它以类模板名作为函数名:

1explicit说明符(可选) 模板名 (形参声明子句) -> 简单模板标识;

推导指引必须指名一个类模板,且必须在类模板的同一语义作用域中,而且对于成员类模板必须拥有同样的访问,但推导指引不会成为该作用域的成员。

推导指引不是函数且没有函数体。推导指引不会被名字查找所找到,并且除了在推导类模板实参时与其他推导指引之间的重载决议之外不会参与重载决议。

不能在同一翻译单元中为同一类模板再次声明推导指引。

推导指引不是必须也得是模板。

 1template<typename T>
 2class Stack {
 3public:
 4    Stack() = default;
 5    Stack(const Stack& s) = default;
 6    Stack(T elem)
 7        : m_cont({std::move(elem)})
 8    {
 9        std::cout << typeid(T).name() << std::endl;
10    }
11
12private:
13    std::vector<T> m_cont;
14};
15
16//推导指引
17Stack(const char*) -> Stack<std::string>; //<--- 注意这一行
18
19int main() {
20    Stack s{"zhxilin"}; //NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE,即std::string
21    //Stack s = "zhxilin"; //编译失败
22    Stack s2{s};
23    Stack s3(s);
24    Stack s4 = s;
25    return 0;
26}

其中Stack(const char*) -> Stack<std::string>;这一句就是推导指引,指定构造函数参数遇到const char*时,把类型T强制推导为std::string类型。

推导指引语句需要与模板类的定义位于相同的作用域中,一般紧跟在模板类的定义后面。在->之后的类型也叫作指导类型。

注意Stack s{"zhxilin"};可以编译成功,但是Stack s = "zhxilin";无法编译通过。这是因为后者是一个拷贝初始化语句,需要先将字符串字面量const char*隐式转换为一个std::string临时变量,再将std::string临时变量隐式转换为Stack。但是C++不支持超过1次隐式转换,因此解析失败,无法通过编译。

s的类型被推导为Stack<std::string>之后,s2s3s4的定义语句其实都是调用的拷贝构造函数,而不是第三个传递单元素的构造函数。

总结

  • 为了实例化一个类模板,需要知道但也不必显式指定每一个模板实参,所以C++17提供了类模板参数推导机制来实现。

  • 利用传入构造函数的实参类型就可以进行类型推导。

  • C++17还提供推导指引语法,可以将匹配成功的构造函数强制指定其推导的结果。

更多关于C++17 CTAD的内容,可以参考cppreference

Prev Post: 『C++14更多新特性』
Next Post: 『C++17结构化绑定』