模板与泛型编程——模板实参推断

一、模板实参推断

  对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数与给定的函数调用最为匹配。

1、类型转换与模板类型参数

  与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。

  顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:

  • const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
  • 数组或函数指针转换:如果形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都是不能应用于函数模板。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template <typename T>
 8 void fobj(T x, T y) { // 实参被拷贝
 9     std::cout << __FUNCTION__ << std::endl;
10 }
11 template <typename T>
12 void fref(const T &x, const T &y) { // 引用
13     std::cout << __FUNCTION__ << std::endl;
14 }
15 int main()
16 {
17     std::string s1("hi");
18     const std::string s2("hello");
19     fobj(s1, s2); // 调用fobj(string,string),const被忽略
20     fref(s1, s2); // 调用fref(const string&,const string&),将s1转换成const是允许的
21
22     int a[10], b[10];
23     fobj(a, b); // 调用f(int*,int*)
24     //fref(a, b); // 错误:数组类型不匹配
25     return 0;
26 }

1)使用相同模板参数类型的函数形参

  一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template <typename T>
 8 bool compare(const T &x, const T &y) {
 9     std::cout << __FUNCTION__ << std::endl;
10     return x == y;
11 }
12 int main()
13 {
14     compare(1, 2.2); // 错误:类型不匹配,一个是int,一个是double
15     return 0;
16 }

2)正常类型转换应用于普通函数实参

  函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理:它们正常转换为对应形参的类型。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template <typename T>
 8 bool compare(const double &x, const T &y) {
 9     std::cout << __FUNCTION__ << std::endl;
10     return x == y;
11 }
12 int main()
13 {
14     compare(1, 2.2); // 正确:1是int,转换为double
15     return 0;
16 }

2、函数模板显式实参

  在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。

1)指定显式模板实参

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template <typename T1, typename T2, typename T3>
 8 T1 sum(const T2 &x, const T3 &y) { // 编译器无法推断T1,它未出现在函数参数列表中
 9     return x + y;
10 }
11 int main()
12 {
13     auto ret = sum<double>(1, 2.2);
14     std::cout << ret << std::endl;
15     return 0;
16 }

在本例中,没有任何函数实参的类型可用来推断T1的类型。每次调用sum时调用者都必须为T1提供一个显式模板实参。

  我们提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。

  显式模板实参按从左至右的顺序与对应的模板实参匹配:第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依次类推。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。

2)正常类型转换应用于显式指定的实参

  对于模板参数已经显式指定了的函数实参,也进行正常的类型转换。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template <typename T>
 8 bool compare(const T &x, const T &y) {
 9     std::cout << __FUNCTION__ << std::endl;
10     return x == y;
11 }
12 int main()
13 {
14     //compare(1, 2.2); // 错误:类型不匹配,一个是int,一个是double
15     compare<int>(1, 2.2); // 正确
16     return 0;
17 }

3、尾置返回类型与类型转换

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 // 由于尾置返回出现在参数列表之后,它可以使用函数的参数
 8 template <typename It>
 9 auto fcn(It beg, It end)->decltype(*beg) {
10     std::cout << __FUNCTION__ << std::endl;
11     return *beg; // 返回序列中一个元素的引用
12 }
13 int main()
14 {
15     std::vector<int> v = { 1,2,3 };
16     std::cout << fcn(v.begin(), v.end()) << std::endl;
17     return 0;
18 }

1)进行类型转换的标准库

  有时,我们希望编写一个类似fcn的函数,但返回一个元素的值而非引用。为了获得元素的类型,我们可以使用标准库的类型转换模板。

对Mod<T>,其中Mod为 若T为 则Mod<T>::type为
remove_reference
X&或X&&

否则


X

T

add_const
X&、const X或函数

否则


T

const T

add_lvalue_reference
X&

X&&

否则


T

X&

T&

add_rvalue_reference
X&或X&&

否则


T

T&&

remove_pointer
X*

或者


X

T

add_pointer
X&或X&&

否则


X*

T*

make_signed
unsigned X

否则


X

T

make_unsigned
带符号类型

否则


unsigned X

T

remove_extent
X[n]

否则


X

T

remove_all_extents
X[n1][b2]...

否则


X

T

每个模板都有一个名为type的public成员,表示一个类型。此类型与模板自身的模板类型相关,其关系如模板名所示。如果不可能(或者不必要)转换模板参数,则type成员就是模板参数类型本身。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 // 由于尾置返回出现在参数列表之后,它可以使用函数的参数
 8 template <typename It>
 9 auto fcn(It beg, It end)->typename std::remove_reference<decltype(*beg)>::type {
10     std::cout << __FUNCTION__ << std::endl;
11     return *beg; // 返回序列中一个元素的拷贝
12 }
13 int main()
14 {
15     std::vector<int> v = { 1,2,3 };
16     std::cout << fcn(v.begin(), v.end()) << std::endl;
17     return 0;
18 }

4、函数指针和实参推断

  当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6
 7 template<typename T>
 8 int compare(const T &x, const T &y) {
 9     if (x < y) return -1;
10     if (x > y) return 1;
11     return 0;
12 }
13 int main()
14 {
15     int(*pf)(const int&, const int&) = compare;
16     std::cout << pf(2, 3) << std::endl;
17     return 0;
18 }

pf中参数的类型决定了T的模板实参的类型。在本例中,T的模板实参类型为int。

  当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。

5、模板实参推断和引用

  为了理解如何从函数调用进行类型推断,考虑下面的例子:

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

其中函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。

1)从左值引用函数参数推断类型

  当一个函数参数是模板类型参数的一个左值引用时,绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 template<typename T> void f(T &p) {
 9     std::cout << __FUNCTION__ << std::endl;
10 }
11 int main()
12 {
13     int i = 1;
14     const int ci = 2;
15     f(i); // T是int
16     f(ci); // T是const int
17     // f(5); // 错误:必须是一个左值才可以
18     return 0;
19 }

  如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面值常量值。当函数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分;因此,它不会也是模板参数类型的一部分。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 template<typename T> void f(const T &p) {
 9     std::cout << __FUNCTION__ << std::endl;
10 }
11 int main()
12 {
13     int i = 1;
14     const int ci = 2;
15     f(i); // T是int
16     f(ci); // T是int
17     f(5); // 一个const &参数可以绑定到一个右值
18     return 0;
19 }

2)从右值引用函数参数推断类型

  当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给它一个右值。推断出的T的类型是该右值实参的类型。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 template<typename T> void f(T &&p) {
 9     std::cout << typeid(p).name() << std::endl;
10 }
11 int main()
12 {
13     f(1024); // 实参是一个int类型的右值,T是int
14     return 0;
15 }

3)引用折叠和右值引用参数

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

假定i是一个int对象,我们可能认为f(i)这样的调用是不合法的。毕竟,i是一个左值,而我们通常不能将一个右值引用绑定到一个左值上。但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础。

  第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f(i)时,编译器推断T的类型为int&。

  T被推断为int&看起来好像意味着f的函数参数应该是一个类型int&的右值引用。通常,我们不能直接定义一个引用的引用。但是通过类型别名或通过模板类型参数间接定义是可以的。

  在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有一种特殊情况下引用会折叠成右值引用;右值引用的右值引用。即,对于一个给定类型X:

  • X& &、X& &&和X&& &都折叠成类型X&。
  • 类型X&& &&折叠成X&&。

  引用折叠只能应用于间接创建的引用,如类型别名或模板参数。

  这个规则导致:如果一个函数参数是接受模板参数类型的右值引用(如T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 template<typename T> void f(T &&p) {
 9     p = 1024;
10 }
11 int main()
12 {
13     int x = 256;
14     f(x);
15     std::cout << x << std::endl;
16     return 0;
17 }

4)编写接受右值引用参数的模板函数

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 template<typename T> void f(T &&p) {
 9     T t = p; // 绑定引用
10     t = 1024;
11     if (t == p)
12         std::cout << "T is &" << std::endl;
13 }
14 int main()
15 {
16     int x = 1;
17     f(x);
18     return 0;
19 }

6、理解std::move

7、转发

  某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值。

1)定义能保持类型信息的函数参数

  通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T&&,通过引用折叠就可以保持实参的左值/右值属性。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7
 8 void g(int &&i) {
 9     std::cout << __FUNCTION__ << ", " << i << std::endl;
10 }
11 template<typename T> void f(T &&t) {
12     g(t);
13 }
14 int main()
15 {
16     f(42);
17     return 0;
18 }

编译器会报错,因为传给g的是名为t的参数。函数参数与其他任何变量一样,都是左值表达式。而g接受的是一个右值表达式。

2)在调用中使用std::forward保持类型信息

  forward定义在头文件utility中,forward必须通过显式模板实参来调用。通常情况下,我们使用forward来传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性。

 1 #include <iostream>
 2 #include <memory>
 3 #include <stdexcept>
 4 #include <string>
 5 #include <vector>
 6 #include <typeinfo>
 7 #include <utility>
 8
 9 void g(int &&i) {
10     std::cout << __FUNCTION__ << ", " << i << std::endl;
11 }
12 template<typename T> void f(T &&t) {
13     g(std::forward<T>(t));
14 }
15 int main()
16 {
17     f(42);
18     return 0;
19 }

如果实参是一个右值,则T是一个普通(非引用)类型,forward<T>将返回T&&。如果实参是一个左值,则通过引用折叠,T本身是一个左值引用类型。在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对forward<T>的返回类型进行引用折叠,将返回一个左值引用类型。

原文地址:https://www.cnblogs.com/ACGame/p/10337664.html

时间: 2024-08-19 19:10:52

模板与泛型编程——模板实参推断的相关文章

C++ Primer 学习笔记_85_模板与泛型编程 --模板特化[续]

模板与泛型编程 --模板特化[续] 三.特化成员而不特化类 除了特化整个模板之外,还可以只特化push和pop成员.我们将特化push成员以复制字符数组,并且特化pop成员以释放该副本使用的内存: template<> void Queue<const char *>::push(const char *const &val) { char *new_item = new char[sizeof(val) + 1]; strncpy(new_item,val,sizeof(

C++ Primer 学习笔记_76_模板和泛型编程 --模板定义[继续]

模板和泛型编程 --模板定义[续] 四.模板类型形參 类型形參由keywordclass或 typename后接说明符构成.在模板形參表中,这两个keyword具有同样的含义,都指出后面所接的名字表示一个类型. 模板类型形參可作为类型说明符在模板中的不论什么地方,与内置类型说明符或类类型说明符的使用方式全然同样. 详细而言,它能够用于指定返回类型或函数形參类型,以及在函数体中用于变量声明或强制类型转换. template <class T> T calc(const T &a,cons

C++ Primer 学习笔记_75_模板与泛型编程 --模板定义

模板与泛型编程 --模板定义 引言: 所谓泛型程序就是以独立于不论什么特定类型的方式编写代码.使用泛型程序时,我们须要提供详细程序实例所操作的类型或值. 模板是泛型编程的基础.使用模板时能够无须了解模板的定义. 泛型编程与面向对象编程一样,都依赖于某种形式的多态性.面向对象编程中的多态性在执行时应用于存在继承关系的类.我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异.仅仅要使用基类的引用或指针,基类类型或派生类类型的对象就能够使用同样的代码. 在泛型编程中,我们所编写的类和函数能够

C++ Primer 学习笔记_84_模板与泛型编程 --模板特化

模板与泛型编程 --模板特化 引言: 我们并不总是能够写出对全部可能被实例化的类型都最合适的模板.某些情况下,通用模板定义对于某个类型可能是全然错误的,通用模板定义或许不能编译或者做错误的事情;另外一些情况下,能够利用关于类型的一些特殊知识,编写比从模板实例化来的函数更有效率的函数. compare函数和 Queue类都是这一问题的好样例:与C风格字符串一起使用进,它们都不能正确工作. compare函数模板: template <typename Type> int compare(cons

C++ Primer 学习笔记_79_模板与泛型编程 --模板编译模型

模板与泛型编程 --模板编译模型 引言: 当编译器看到模板定义的时候,它不立即产生代码.只有在用到模板时,如果调用了函数模板或定义了模板的对象的时候,编译器才产生特定类型的模板实例. 一般而言,当调用函数时[不是模板],编译器只需看到函数的声明.类似的,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的.因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中. 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码.当调用函数模板或类模板的成员函

C++ Primer 学习笔记_76_模板与泛型编程 --模板定义[续]

模板与泛型编程 --模板定义[续] 四.模板类型形參 类型形參由keywordclass或 typename后接说明符构成.在模板形參表中,这两个keyword具有同样的含义,都指出后面所接的名字表示一个类型. 模板类型形參可作为类型说明符在模板中的不论什么地方,与内置类型说明符或类类型说明符的使用方式全然同样.详细而言,它能够用于指定返回类型或函数形參类型,以及在函数体中用于变量声明或强制类型转换. template <class T> T calc(const T &a,const

C++模板实参推断

1 类型转换与模板实参 1)自动转换的只有:const转换, 数组及函数到指针的转换 注:不同大小相同元素类型是不同的类型 2)相同模板参数名对应的实参类型必须相同 3)不同模板参数名对应的实参类型可以不同,但必须兼容 2 函数模板的返回值问题 函数模板只会对函数参数列表的类型进行推断不会对返回值推断 解决方法: 1) 显示模板参数 注: 显示指定了模板类型参数在类型转换上和普通函数参数一样 template <typename T1, typename T2, typename T3> T1

C++ primer-&gt;16.2 模板实参推断

一.类型转换与模板类型参数 1.如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则.只有很有限的几种类型转换会自动地应用于这些实参. ①.顶层const无论是在形参中还是在实参中,都会被忽略. ②.const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参. ③.数组或函数指针转换:如果函数形参不是引用类型,则可以将对数组或函数类型的实参应用于正常的指针转换. 如下程序所示: 1 template<typename T> T fobj(

【C++ Primer 第16章】2. 模板实参推断

模板实参推断:对于函数模板,编译器利用调用中的函数实参来确定模板参数,从函数实参来确定模板参数的过程被称为模板实参推断. 类型转换与模板类型参数 与往常一样,顶层const无论在形参中还是在是实参中,都被会忽略. • const转换:可以将一个非const对象的引用(或指针)传递给const的引用(或指针)形参. • 数组或函数指针转换:一个数组实参可以转换为一个指向其首元素的指针.类似的,一个函数实参可以抓转换一个该函数类型的指针. 1 template <typename T> T fob