[C++模板] --- 函数模板

简介: [C++模板] --- 函数模板

1. 为什么需要函数模板

在泛型编程出现前,我们要实现一个swap函数得这样写:

void swap(int &a, int &b) {
    int tmp{a};
    a = b;
    b = tmp;
}

但这个函数只支持int型的变量交换,如果我们要做float, long, double, std::string等等类型的交换时,只能不断加入新的重载函数。这样做不但代码冗余,容易出错,还不易维护。C++函数模板有效解决了这个问题。函数模板摆脱了类型的限制,提供了通用的处理过程,极大提升了代码的重用性。

2. 函数模板使用

2.1 函数模板语法

函数模板作用:

建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

函数模板语法:

template<typename T>
函数声明或定义

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

2.2 使用非类型形参

#include <iostream>
// N必须是编译时的常量表达式
template<typename T, int N>
void printArray(const T (&a)[N]) {
    std::cout << "[";
    const char *sep = "";
    for (int i = 0; i < N; i++, (sep = ", ")) {
        std::cout << sep << a[i];
    }
    std::cout << "]" << std::endl;
}
int main() {
    // T: int, N: 3
    int a[] = {1,2,3};
    printArray(a);
}
//输出:[1, 2, 3]

T (&a)[N]表明a是一个引用,其引用的数据类型是T[N],也即一个数组。

2.3 返回值为auto

有些时候我们会碰到这样一种情况,函数的返回值类型取决于函数参数某种运算后的类型。对于这种情况可以采用auto关键字作为返回值占位符。

template<typename T1, typename T2>
auto multi(T a, T b) -> decltype(a * b) {
    return a * b;
}

decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的是解决泛型编程中有些类型由模板参数决定,而难以表示的问题。为何要将返回值后置呢?

// 这样是编译不过去的,因为decltype(a*b)中,a和b还未声明,编译器不知道a和b是什么。
template<typename T1, typename T2>
decltype(a*b) multi(T a, T b) {
    return a*+ b;
}
//编译时会产生如下错误:error: use of undeclared identifier 'a'

2.4 类成员函数模板

函数模板可以做为类的成员函数。

#include <iostream>
class object {
public:
    template<typename T>
    void print(const char *name, const T &v) {
        std::cout << name << ": " << v << std::endl;
    }
};
int main() {
    object o;
    o.print("name", "Crystal");
    o.print("age", 18);
}

输出:

name: Crystal
age: 18

需要注意的是:函数模板不能用作虚函数。这是因为C++编译器在解析类的时候就要确定虚函数表(vtable)的大小,如果允许一个虚函数是函数模板,那么就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。

2.5 函数模板重载

函数模板之间、普通函数和模板函数之间可以重载。编译器会根据调用时提供的函数参数,调用能够处理这一类型的最佳匹配版本。在匹配度上,一般按照如下顺序考虑:

1 最符合函数名和参数类型的普通函数

2 特殊模板(具有非类型形参的模板,即对T有类型限制)

3 普通模板(对T没有任何限制的)

4 通过类型转换进行参数匹配的重载函数

#include <iostream>
template<typename T>
const T &max(const T &a, const T &b) {
    std::cout << "max(&, &) = ";
    return a > b ? a : b;
}
// 函数模板重载
template<typename T>
const T *max(T *a, T *b) {
    std::cout << "max(*, *) = ";
    return *a > *b ? a : b;
}
// 函数模板重载
template<typename T>
const T &max(const T &a, const T &b, const T &c) {
    std::cout << "max(&, &, &) = ";
    const T &t = (a > b ? a : b);
    return t > c ? t : c;
}
// 普通函数
const char *max(const char *a, const char *b) {
    std::cout << "max(const char *, const char *) = ";
    return strcmp(a, b) > 0 ? a : b;
}
int main() {
    int a = 1, b = 2;
    std::cout << max(a, b) << std::endl;
    std::cout << *max(&a, &b) << std::endl;
    std::cout << max(a, b, 3) << std::endl;
    std::cout << max("en", "ch") << std::endl;
    // 可以通过空模板实参列表来限定编译器只匹配函数模板
    std::cout << max<>("en", "ch") << std::endl;
}

编译输出:

max(&, &) = 2
max(*, *) = 2
max(&, &, &) = 3
max(const char *, const char *) = en
max(*, *) = en

可以通过空模板实参列表来限定编译器只匹配函数模板,比如main函数中的最后一条语句

3. 函数模板不是函数

函数模板用来定义一族函数,而不是一个函数。C++是一种强类型的语言,在不知道T的具体类型前,无法确定swap需要占用的栈大小(参数栈,局部变量),同时也不知道函数体中T的各种操作如何实现,无法生成具体的函数。只有当用具体类型去替换T时,才会生成具体函数,该过程叫做函数模板的实例化。

当在main函数中调用swap(a,b)时,编译器推断出此时T为int,然后编译器会生成int版的swap函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。

4. 其它

4.1 函数模板 .vs. 模板函数

函数模板重点在模板。表示这是一个模板,用来生成函数。

模板函数重点在函数。表示的是由一个模板生成而来的函数。

4.2 cv限定

cv限定是指函数参数中有const、volatile或mutable限定。已指定、推导出或从默认模板实参获得所有模板实参时,函数参数列表中每次模板形参的使用都会被替换成对应的模板实参。替换后:

所有数组类型和函数类型参数被调整成为指针

所有顶层cv限定符从函数参数被丢弃,如在普通函数声明中。

顶层cv限定符的去除不影响参数类型的使用,因为它出现于函数中:

template <typename T> void f(T t);
template <typename X> void g(const X x);
template <typename Z> void h(Z z, Z *zp);
// 两个不同函数有同一类型,但在函数中, t有不同的cv限定
f<int>(1);       // 函数类型是 void(int) , t 为 int
f<const int>(1); // 函数类型是 void(int) , t 为 const int
// 二个不同函数拥有同一类型和同一 x
// (指向此二函数的指针不相等,且函数局域的静态变量可以拥有不同地址)
g<int>(1);       // 函数类型是 void(int) , x 为 const int
g<const int>(1); // 函数类型是 void(int) , x 为 const int
// 仅丢弃顶层 cv 限定符:
h<const int>(1, NULL); // 函数类型是 void(int, const int*) 
                       // z 为 const int , zp 为 int*


目录
打赏
0
0
0
0
20
分享
相关文章
|
3月前
|
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
136 10
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
91 4
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
40 3
|
2月前
|
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
40 0
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
28 1
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
133 6
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
59 9
|
3月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
53 0
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
43 3
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等