IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 函数模板 Function Templates -> 正文阅读

[C++知识库]函数模板 Function Templates

从本文开始,对 C++ Templates The Complete Guide Second Edition 进行系统学习和解读。

本章是对第一章模板函数的基本知识的讲解,后续章节会有更深的探讨。

函数模板惊鸿一瞥 A First Look at Function Templates

一个函数模板表示函数族,它的一些元素被参数化。

定义函数模板 Defining the Template

template<typename T>
T max (T a, T b) {
  return b < a ? a : b;
}

该模板函数返回两个输入参数中的最大值,两个输入参数 ab 的类型为模板参数 T。模板参数按照如下语法形式进行申明:

template< comma-separated-list-of-parameters >

这里,关键字 typename 用于定义模板参数。由于历史原因,也可以使用关键字 class 代替 typename 来定义模板参数。在 C++98 之前是使用 class 申明模板参数,C++98 引入了 typename,为了保持兼容,二者皆可。因此,上述代码等价如下:

template<class T>
T max (T a, T b) {
  return b < a ? a : b;
}

但是,为了避免和类(class)的定义混淆,建议优先使用 typename 定义模板类型参数。

在我们的例子中模板参数为 T,使用 T 表示只是惯例,你也可以使用其他标识符,例如 U。这里的模板参数可以是任意类型,只要这种类型满足模板函数对其的操作。在上面的例子中,类型 T 必须支持 < 操作。此外,不太明显的一个限制是:为了被函数返回,T 类型的值必须是可以拷贝的。

使用函数模板 Using the Template

int main()
{
  int i = 42;
  std::cout << "max(7,i): " << ::max(7,i) << ’\n’;  // 42
  double f1 = 3.4;
  double f2 = -6.7;
  std::cout << "max(f1,f2): " << ::max(f1,f2) << ’\n’; // 3.4
  std::string s1 = "mathematics";
  std::string s2 = "math";
  std::cout << "max(s1,s2): " << ::max(s1,s2) << ’\n’;  // mathematics
  return 0;
}

这里使用 ::max 调用函数 max 可以避免混淆,是为了在全局的命名空间查找 max,因为标准库中也定义了该函数 std::max

使用不同参数类型的函数调用会产生不同的函数实例。上面的例子分别产生 intdoublestd::string 3 个版本的实例。例如 ::max(7,i) ,编译器会产生参数类型为 int 的函数实例,类似调用如下代码:

int max (int a, int b) {
  return b < a ? a : b;
}

注意,void 也是有效的模板参数类型。例如:

template<typename T>
T foo(T*) {
}

void* vp = nullptr;
foo(vp); // OK: deduces void foo(void*)

两个阶段的翻译过程 Two-Phase Translation

使用模板中操作不支持的参数类型会产生编译报错。例如:

std::complex<float> c1, c2; // doesn’t provide operator <
...
::max(c1,c2); // ERROR at compile time

模板的翻译/编译分为两个阶段:分别发生在实例化前和实例化期间。

  • 实例化前检查代码模板代码,这时候忽略模板参数。主要包括:
    • 是否有语法错误。比如是否遗漏分号。
    • 是否使用不依赖模板参数的未知名字(函数名、类型名等)。
    • 检查不依赖模板参数的 static assert。
  • 实例化期间再次检查模板代码,特别是依赖模板参数的部分。例如上面例子中实例化的参数类型是否支持 < 操作。
template <typename T>
void f(T x) {
  undeclared();   // first-phase compile-time error if undeclared() unknown
  undeclared(x);  // second-phase compile-time error if undeclared(T) unknown
  static_assert(sizeof(int) > 10,
                "int too small");  // always fails if sizeof(int) <= 10
  static_assert(sizeof(T) > 10,
                "T too small");  // fails if instantiated for T with size <= 10
}

模板参数推导 Template Argument Deduction

当我们使用参数调用模板函数(比如 max()),模板参数就由我们传递的参数决定。如果我们传递两个 int 给模板函数,编译器将推导出 Tint

另外,T 可能是类型的一部分。例如,我们使用常量引用申明 max()

template<typename T>
T max (T const& a, T const& b) {
  return b < a ? a : b;
}

我们再次传递两个 int 给该模板函数,T 也被推导成 int

类型推导过程中的类型转换:

  • 当通过引用申明调用参数时,类型推到过程中不会进行任何类型转化,两个被申明成相同模板参数 T 的参数必须完全匹配。例如:
#include<iostream>

 template <typename T>
 T max(T& a, T& b) {
     return b < a ? a : b;
 }
 
 int main() {
     const int a = 3;
     int b = 4;
     std::cout << ::max(a, b) << std::endl;
     return 0;
 }

编译报错如下:

hello.cpp: In function ‘int main():
hello.cpp:11:28: error: no matching function for call to ‘max(const int&, int&)11 |     std::cout << ::max(a, b) << std::endl;
      |                            ^
hello.cpp:4:3: note: candidate:template<class T> T max(T&, T&)4 | T max(T& a, T& b) {
      |   ^~~
hello.cpp:4:3: note:   template argument deduction/substitution failed:
hello.cpp:11:28: note:   deduced conflicting types for parameter ‘T’ (const intandint)
   11 |     std::cout << ::max(a, b) << std::endl;
      |                            ^
  • 当通过值申明调用参数时,只支持退化(decay)的类型转换:cv(const 和 volatile)被忽略,引用转换为被引用的类型,原始数组和函数转换为相应的指针类型。两个被申明成相同模板参数 T 的参数的退化类型必须匹配。例如:
template<typename T>
T max (T a, T b);
...
int i = 5;
int const c = 42;
max(i, c);    // OK: T is deduced as int
max(c, c);    // OK: T is deduced as int
int& ir = i;
max(i, ir);   // OK: T is deduced as int
int arr[4];
foo(&i, arr); // OK: T is deduced as int*

但是下面的例子就有问题:

max(4, 7.2);      // ERROR: T can be deduced as int or double
std::string s;
foo("hello", s);  // ERROR: T can be deduced as char const[6] or std::string

解决上面这两个例子问题的方式如下:

  1. 显示强制类型转换。max(static_cast<double>(4), 7.2); // OK
  2. 显示指定模板参数以阻止编译器的类型推导。max<double>(4, 7.2); // OK
  3. 使用两个不同类型的模板参数。

函数默认调用参数的类型推导:

函数默认调用参数无法推导类型。看个例子:

template<typename T>
void f(T = "");
...

f(1); // OK: deduced T to be int, so that it calls f<int>(1)
f();  // ERROR: cannot deduce T

为了解决这个问题,需要申明模板参数默认类型。

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

多模板参数 Multiple Template Parameters

可以根据需要指定模板参数的个数。例如:

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
  return b < a ? a : b;
}
...
auto m = ::max(4, 7.2); // OK, but type of first argument defines return type

从语法上看,上述代码没有任何问题。只是使用模板参数之一作为返回类型,那么调换下两个变量调用顺序,则会得到不同的结果。传递 66.6642 max 得到 double 类型的 66.66,而传递 4266.66max 得到 int 类型的 66

C++ 提供了不同的方式解决上述问题:

  • 引入第 3 个模板参数作为返回类型。
  • 让编译器推导出返回类型。
  • 使用两个参数的公共类型(common type)作为返回类型。

接下来分别讨论以上方式。

返回值类型的模板参数 Template Parameters for Return Types

在模板函数调用时,我们既可以显示指定模板参数类型,也可以不指定(交给编译器推导)。但是,当调用参数和模板参数之间没有联系的情况下,你必须在调用模板函数时显示指定模板参数。

template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);

对于这种情况,编译器的参数推导无法处理返回值的类型,RT 也没有出现在函数调用参数中,因此 RT 无法被推导。你必须显示指定模板参数类型:

::max<int,double,double>(4, 7.2); // OK, but tedious

通常,你必须指定所有的参数类型直到最后一个无法被隐式推导的参数。对于这个例子,我们可以调整下返回值类型模板参数位置,这样我们可以在函数调用时候只指定返回值的类型,其他模板参数交给编译器推导。

template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);
...
::max<double>(4, 7.2) // OK: return type is double, T1 and T2 are deduced

推导返回值类型 Deducing the Return Type

如果返回值类型依赖模板参数,最好的方法是交给编译器推导。C++14 可以直接使用 auto 作为返回值类型。

template<typename T1, typename T2>
auto max (T1 a, T2 b) {
  return b < a ? a : b;
}

这里没有使用尾置返回类型(trailing return type)表明返回值类型可以根据函数体中返回值语句推导。在 C++11 中,需要使用尾置返回值类型声明:

template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b) {
  return b < a ? a : b;
}

返回值的类型由问号表达式 b<a?a:b 的类型来确认。如果 a 和 b 的类型不同,decltype(b<a?a:b) 将返回二者的公共类型(common type)。关于问号表达式的返回类型可以参考 Conditional Operator: ? : ,关于公共类型可以参考 std::common_type

因此,这里问号表达式的条件并不重要,可以直接使用 true 代替:

template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(true?a:b);

然后,上面的代码还有一个缺陷:返回值的类型可能是一个引用类型,因为有些条件下 T 可能是一个引用。因而需要改造如下:

#include <type_traits>
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> typename std::decay<decltype(true?a:b)>::type
{
  return b < a ? a : b;
}

这里的 type 是一个成员类型,需要使用关键字 typename 进行修饰。

返回类型为通用类型 Return Type as Common Type

C++11 标准库提供了 std::common_type<>::type 产生两个(或多个)类型的公共类型(common type)。因此,我们可以使用 std::common_type<>::type 指定模板函数返回值类型:

#include <type_traits>
template<typename T1, typename T2>
typename std::common_type<T1,T2>::type max (T1 a, T2 b) {
  return b < a ? a : b;
}

C++14 简化了 trait 库的使用,直接在 trait name 后加 _t 后缀。上面的返回值类型可以简化为:

std::common_type_t<T1,T2> // equivalent since C++14

默认模板参数 Default Template Arguments

我们可以定义模板参数的默认值。例如上面的例子,我们可以定义 max 的返回值类型 RT,并将其默认值定义为 T1T2common_type,使用问号表达式实现如下:

#include <type_traits>
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}

注意:以上实现,需要我们能够调用传入类型的默认构造函数。即 T1T2 必须有默认构造函数。例如:

#include <type_traits>

class A {
public:
    A() = default;
    A(int a): x(a) {}
    bool operator< (const A& a) {
        return this->x < a.x;
    }
private:
    int x;
};

template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
    return b < a ? a : b;
}

int main()
{   
    A a(2), b(3);
    auto c = ::max(a, b);
    return 0;
}

如果将 A() = default; 注释掉,则 A 没有默认构造函数,则导致模板默认参数类型推导失败。

main.cpp:15:44: error: no matching function for call to 'A::A()'
   15 | typename RT = std::decay_t<decltype(true ? T1() : T2())>>

也可以使用 std::common_type 指定默认模板参数的公共类型:

#include <type_traits>
template<typename T1, typename T2,
// typename RT = typename std::common_type<T1,T2>::type> // C++11
typename RT = std::common_type_t<T1,T2>> // C++14
RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}

我们可以使用默认模板参数作为返回值类型,也可以指定返回值类型:

auto a = ::max(4, 7.2);
auto b = ::max<double,int,long double>(7.2, 4);

如前面介绍的,我们可以调整默认模板参数位置到第一个,这样调用的时候可以只指定返回值类型,其他模板参数类型交给编译器推导。

template<typename RT = long, typename T1, typename T2>
RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}

int i;
long l;
...
max(i, l); // returns long (default argument of template parameter for return type)
max<int>(4, 42); // returns int as explicitly requested

函数模板重载 Overloading Function Templates

像普通函数一样,函数模板也可以重载。例如:

// 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 argument deduction)
  ::max<double>(7, 42); // calls max<double> (no argument deduction)
  ::max('a', 42.7); // calls the nontemplate for two ints
}

C++ 重载函数的匹配原则:如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本。关于这点可以参看我之前的博客 Item 26: Avoid overloading on universal references.

对于 ::max(7, 42); 普通函数和模板函数都可以匹配,优先匹配普通函数。对于 ::max(7.0, 42.0) 则会匹配到模板函数。

当然也可以显示指定一个空的模板参数列表,用于表明调用函数模板而不是普通函数:

::max<>(7, 42); // calls max<int> (by argument deduction)

普通函数的参数可以自动进行类型转化,而函数模板不行。因而最后一个调用普通函数,两个参数都转为 int

::max('a', 42.7); // only the nontemplate function allows nontrivial conversions

对函数模板重载需要保证:对于任何调用,只有其中一个版本能够匹配,否则产生歧义。例如:返回值类型重载可能导致歧义。

template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
  return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
  RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}

auto a = ::max(4, 7.2); // uses first template

但是,auto b = ::max<long double>(7.2, 4); 则两个版本都可以匹配,导致编译报错:

main.cpp:17:33: error: call of overloaded 'max<long int, double>(int, double)' is ambiguous
   17 |     auto c = ::max<long, double>(4, 7.2);
      |              ~~~~~~~~~~~~~~~~~~~^~~~~~~~
main.cpp:4:6: note: candidate: 'auto max(T1, T2) [with T1 = long int; T2 = double]'
    4 | auto max (T1 a, T2 b)
      |      ^~~
main.cpp:9:4: note: candidate: 'RT max(T1, T2) [with RT = long int; T1 = double; T2 = double]'
    9 | RT max (T1 a, T2 b)

在看一个指针和传统 C 字符串重载的例子:

#include <cstring>
#include <string>
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
	return b < a ? a : b;
}
// maximum of two pointers:
template<typename T>
T* max (T* a, T* b)
{
	return *b < *a ? a : b;
}
// maximum of two C-strings:
char const* max (char const* a, char const* b)
{
	return std::strcmp(b,a) < 0 ? a : b;
}
int main ()
{
	int a = 7;
	int b = 42;
	auto m1 = ::max(a,b); // max() for two values of type int
	std::string s1 = "hey";
	std::string s2 = "you";
	auto m2 = ::max(s1,s2); // max() for two values of type std::string
	int* p1 = &b;
	int* p2 = &a;
	auto m3 = ::max(p1,p2); // max() for two pointers
	char const* x = "hello";
	char const* y = "world";
	auto m4 = ::max(x,y); // max() for two C-strings
}

一般建议使用传值的函数模板,如果实现的传引用的模板函数,再重载传 C 字符串值版本,可能导致错误:

#include <cstring>
// maximum of two values of any type (call-by-reference)
template<typename T>
T const& max (T const& a, T const& b)
{
	return b < a ? a : b;
}
// maximum of two C-strings (call-by-value)
char const* max (char const* a, char const* b)
{
	return std::strcmp(b,a) < 0 ? a : b;
}
// maximum of three values of any type (call-by-reference)
template<typename T>
T const& max (T const& a, T const& b, T const& c)
{
	return max (max(a,b), c); // error if max(a,b) uses call-by-value
}
int main ()
{
	char const* s1 = "frederic";
	char const* s2 = "anica";
	char const* s3 = "lucas";
	auto m2 = ::max(s1, s2, s3); // run-time ERROR
}

max(max(a, b), c) 中,max(a, b) 产生了一个临时对象的引用,这个临时引用在 return max (max(a,b), c); 语句结束就失效了,导致引用悬挂:

main.cpp: In instantiation of 'const T& max(const T&, const T&, const T&) [with T = const char*]':
main.cpp:24:17:   required from here
main.cpp:17:20: warning: returning reference to temporary [-Wreturn-local-addr]
   17 |         return max (max(a,b), c); // error if max(a,b) uses call-by-value
      |                ~~~~^~~~~~~~~~~~~
bash: line 7:  2907 Segmentation fault      (core dumped) ./a.out

此外,重载版本需要在调用前申明,否则可能产生不符合预期的结果。例如:

#include <iostream>
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
std::cout << "max<T>() \n";
	return b < a ? a : b;
}
// maximum of three values of any type:
template<typename T>
T max (T a, T b, T c)
{
	return max (max(a,b), c); // uses the template version even for ints
}                             // because the following declaration comes
                              // too late:
// maximum of two int values:
int max (int a, int b)
{
std::cout << "max(int,int) \n";
	return b < a ? a : b;
}
int main()
{
	::max(47,11,33); // OOPS: uses max<T>() instead of max(int,int)
}

此外,C++11 可以使用 constexpr 函数来生成编译期值。

template <typename T, typename U>
constexpr auto max(T a, U b) {
  return b < a ? a : b;
}

int a[::max(sizeof(char), 1000u)];
std::array<int, ::max(sizeof(char), 1000u)> b;

总结 Summary

  • 函数模板定义了一个函数族
  • 编译器可以根据传递的参数推导模板参数
  • 也可以显示指定模板参数
  • 可以定义默认模板参数
  • 可以重载函数模板
  • 重载模板函数时,需要确保对于任何调用只有一个匹配版本
  • 重载函数模板时,注意传引用和传值版本导致的引用悬挂问题
  • 重载版本需要在调用前申明

参考 Reference

  • http://www.tmplbook.com
  • https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/e4213hs1%28v=vs.110%29?redirectedfrom=MSDN
  • https://en.cppreference.com/w/cpp/types/common_type
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-16 21:31:10  更:2022-06-16 21:31:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 23:35:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码