C++泛型编程之函数模板

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: C++泛型编程之函数模板

前言

C++的泛型编程是指通过使用模板技术来实现通用的代码,使得同一段代码可以适用于不同类型的数据,从而提高代码的重用性和灵活性。


在C++中,泛型编程主要通过使用函数模板和类模板来实现。函数模板是一种允许定义通用函数的机制,它可以接受不同类型的参数,并根据实际参数类型推导出最适合的函数实例。类模板允许定义通用类,其中的成员函数和成员变量可以具有通用的类型,从而使得同一套代码适用于不同类型的对象。


泛型编程的优势在于可以提高代码的可重用性和可扩展性。通过编写泛型代码,开发人员可以减少代码的冗余,减少维护工作,并且可以将关注点集中在算法和逻辑上,而不是为每个特定类型编写特定的代码。此外,泛型编程还能提供更好的类型安全性,因为编译器会对模板参数的类型进行检查。



一、函数模板

函数模板是C++中一种用来定义通用函数的机制。它允许我们编写一段代码,可以适用于不同类型的参数,从而实现通用的功能。


函数模板以关键字`template`开始,后跟模板参数列表,其中包含一个或多个类型参数。在函数模板中,我们可以使用这些类型参数作为函数的参数类型、返回类型或局部变量的类型。

下面是一个函数模板的示例:

template <typename T>
T add(T a, T b) {
    return a + b;
}

在上面的示例中,`template <typename T>`表示这是一个函数模板,`T` 是一个类型参数。`add` 函数可以接受两个类型为 `T` 的参数,并返回它们的和。当调用 `add` 函数时,编译器会根据函数参数的实际类型来推断出正确的函数实例。


例如,我们可以使用 `add` 函数来对整数、浮点数或其他类型的参数进行加法运算,而无需编写多个重载函数。

int x = add(3, 5);           // 调用 add<int>(3, 5)
double y = add(2.5, 3.7);    // 调用 add<double>(2.5, 3.7)

函数模板在编译时生成适当类型的函数实例,从而提供了代码的灵活性和重用性。

1 函数模版特化

函数模板的特化(template specialization)是一种针对特定模板参数类型提供特定实现的机制。通过函数模板的特化,我们可以根据具体的类型需求,为特定的模板参数类型提供定制化的函数实现。


函数模板的特化可以分为:


1. 全特化(Full Specialization):对于某个具体的模板参数类型,我们可以提供完全定制化的实现。全特化的函数模板定义中的模板参数将被具体类型所取代,从而生成一个与原始模板不同的函数。

以下是一个全特化的示例:

template<>
int add<int>(int a, int b) {
  return a + b + 10;
}

在上述示例中,`add` 函数针对 `int` 类型进行全特化,实现了一个特定的加法实现,并在结果上添加了一个额外的偏移量。


2. 部分特化(Partial Specialization):在某些情况下,我们可能只想特化模板的一部分参数,而将其他参数保持为通用实现。这种情况下,我们可以使用部分特化。


以下是一个部分特化的示例:

template <typename T, typename U>
struct Pair {
  T first;
  U second;
};
template <typename T>
struct Pair<T, T> {
  T both;
};

在上述示例中,`Pair` 是一个接受两个类型参数的模板结构体。通过部分特化,我们特化了 `Pair` 结构体的第二个类型参数为 `T` 的情况,使得 `Pair<T, T>` 的对象只有一个成员变量 `both`。


函数模板的特化允许我们根据不同的类型需求,提供定制化的函数实现,从而进一步扩展函数模板的灵活性和可用性。特化的函数模板将根据特定的类型参数生成独立的函数,从而提供了更加精确和定制的行为。

1.1 C++代码示例

#include <iostream>
// 函数模板
template <typename T>
T add(T a, T b) {
  return a + b;
}
// 函数模板的特化
template <>
float add<float>(float a, float b) {
  return a + b + 0.5f;
}
int main() {
  int x = add(3, 5);                    // 调用通用的 add 函数
  float y = add<float>(2.5f, 3.7f);     // 调用特化的 add 函数
  std::cout << "Int result: " << x << std::endl;
  std::cout << "Float result: " << y << std::endl;
  return 0;
}

结果:

Int result: 8
Float result: 6.2

2 默认模板参数

默认模板参数(Default Template Arguments)是指在定义模板时为一个或多个模板参数指定默认值。当使用这些模板时,如果没有为对应的参数提供具体的值,编译器将使用默认的参数值。

默认模板参数的语法形式为在模板参数列表中为相应的参数赋予默认值。

下面是一个示例:

template <typename T = int, int N = 5>
void printArray() {
  for (int i = 0; i < N; ++i) {
    T value{};
    std::cout << value << " ";
  }
  std::cout << std::endl;
}

在上述代码中,我们定义了一个模板函数 `printArray`,它具有两个模板参数:类型参数 `T` 和整数参数 `N`。使用默认模板参数,我们为 `T` 设置了默认类型为 `int`,为 `N` 设置了默认值为 `5`。


当我们在使用 `printArray` 函数时,如果没有指定类型参数和整数参数的具体值,将使用默认的参数值:

printArray();             // 使用默认的参数类型和参数值
printArray<double, 10>(); // 指定类型参数为 double,使用默认的整数参数值
printArray<char>();       // 指定类型参数为 char,使用默认的整数参数值

在上述示例中,第一个调用使用了默认的模板参数类型 `int` 和参数值 `5`。第二个调用指定了类型参数为 `double`,但仍然使用了默认的整数参数值 `5`。第三个调用指定了类型参数为 `char`,同样使用默认的整数参数值 `5`。


默认模板参数允许我们在定义模板时预设一些常用的参数值,以提供更大的灵活性和便捷性。它使得模板可以更方便地使用,并为用户提供了更简单的接口。


3 可变参数模板

函数模板的可变参数模板(Variadic Template)是 C++11 引入的一个特性,它允许函数模板接受任意数量的参数。


以往在编写函数模板时,参数的数量是固定的,无法接受可变数量的参数。可变参数模板通过使用特殊的语法,使得函数模板可以接受任意数量的参数,并且在函数体中可以对这些参数进行处理。


可变参数模板通过使用 `...`(省略号)表示可变数量的参数。


以下是一个使用可变参数模板的示例:

#include <iostream>
// 可变参数模板
template<typename... Args>
void printValues(Args... args) {
  std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
  std::cout << "Values: ";
  (std::cout << ... << args) << std::endl;  // 使用折叠表达式展开参数列表
}
int main() {
  printValues(1, 2, 3, 4);               // 输出:Number of arguments: 4, Values: 1 2 3 4
  printValues("Hello", 3.14, 'a');       // 输出:Number of arguments: 3, Values: Hello 3.14 a
  return 0;
}

在上述代码中,我们定义了一个 `printValues` 函数模板,并使用 `Args...` 作为可变参数模板参数。在函数体内部,我们使用了折叠表达式(fold expression)来展开参数列表,并输出参数的数量和值。


在 `main` 函数中,我们演示了使用不同数量和类型的参数调用 `printValues` 函数模板,并得到了相应的输出结果。


可变参数模板使得函数模板能够接受任意数量的参数,并且可以对这些参数进行操作。这在编写通用和灵活的函数模板时非常有用,可以处理各种不同数量和类型的参数。

4 模板元编程

函数模板的模板元编程(Template metaprogramming,TMP)是一种技术,利用 C++ 的模板机制实现在编译时进行计算和计算类型的能力。通过函数模板的特化、模板递归、常量表达式等特性,可以在编译期间生成代码,实现一些高度通用和灵活的计算。


函数模板的模板元编程具有以下特点:


1. 编译时计算:模板元编程利用编译时的计算能力,在编译期间生成代码,而不是在程序运行时进行计算。这使得模板元编程可以在编译时进行更高效、更灵活的计算。


2. 基于模板:模板元编程使用 C++ 的模板机制,通过定义和使用函数模板进行元编程。函数模板的参数和返回值可以是常量、类型或者其他函数模板,从而实现复杂的计算。


3. 模板特化:在模板元编程中,可以对函数模板进行特化,为特定的参数类型提供特定的实现。通过模板特化,可以根据不同的条件或参数类型生成不同的代码。


4. 模板递归:模板元编程常常使用模板递归的技巧,通过递归的方式在编译期间进行计算。这使得模板元编程可以处理任意复杂度的计算问题。


函数模板的模板元编程在各种场景下都有广泛的应用,例如在编译期间计算数值、类型转换、类型判断、条件编译等。通过合理地应用模板元编程,可以实现高效、灵活且类型安全的代码生成和计算。然而,模板元编程技术相对较为复杂,需要一定的经验和理解才能使用和理解。

4.1 C++代码简单示例

#include <iostream>
// 模板元编程实现斐波那契数列
template <int N>
struct Fibonacci {
    static const int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};
template <>
struct Fibonacci<0> {
    static const int value = 0;
};
template <>
struct Fibonacci<1> {
    static const int value = 1;
};
int main() {
    const int index = 10;
    std::cout << "Fibonacci(" << index << ") = " << Fibonacci<index>::value << std::endl;
    return 0;
}

在这个示例中,我们使用模板元编程计算第 N 个斐波那契数,其中 N 在编译时确定。模板结构体 `Fibonacci` 被用作递归模板,计算 `Fibonacci<N>` 的值。


我们通过为结构体 `Fibonacci` 添加特化的模板结构体,分别定义了 `Fibonacci<0>` 和 `Fibonacci<1>` 的值,作为递归的基准情况。对于其他大于 1 的 N,`Fibonacci<N>` 的值通过递归调用 `Fibonacci<N - 1>` 和 `Fibonacci<N - 2>` 的值求得。


在 `main` 函数中,我们指定了要计算的斐波那契数列的索引为 `10`,然后使用 `Fibonacci<index>::value` 输出了对应的斐波那契数。


这个示例展示了模板元编程的基本思想和用法,通过在编译时进行递归计算,我们可以在运行时获得斐波那契数的值。请注意,在实际的模板元编程中,通常会结合更多的技巧和用法,实现更复杂的计算和类型操作。

4.2 C++代码一般示例

#include <iostream>
// --------------------
// 定义类型列表
// --------------------
// 类型列表的基本实体
template <typename... Ts>
struct TypeList {};
// --------------------
// 获取类型列表长度
// --------------------
// 元编程计算类型列表长度的辅助结构体
template <typename TypeList>
struct Length;
// 特化:空类型列表长度为 0
template <>
struct Length<TypeList<>> {
    static constexpr int value = 0;
};
// 递归:非空类型列表长度为当前类型 + 剩余类型的长度
template <typename T, typename... Ts>
struct Length<TypeList<T, Ts...>> {
    static constexpr int value = 1 + Length<TypeList<Ts...>>::value;
};
// --------------------
// 查找类型在类型列表中的位置
// --------------------
// 元编程查找类型在类型列表中位置的辅助结构体
template <typename TypeList, typename T, int Index = 0>
struct IndexOf;
// 特化:找到目标类型,返回当前索引值
template <typename T, typename... Ts, int Index>
struct IndexOf<TypeList<T, Ts...>, T, Index> {
    static constexpr int value = Index;
};
// 递归:继续在剩余类型中查找
template <typename T, typename U, typename... Ts, int Index>
struct IndexOf<TypeList<U, Ts...>, T, Index> {
    static constexpr int value = IndexOf<TypeList<Ts...>, T, Index + 1>::value;
};
// 特化:未找到目标类型,返回 -1
template <typename T, int Index>
struct IndexOf<TypeList<>, T, Index> {
    static constexpr int value = -1;
};
// --------------------
// 判断类型列表中是否包含某个类型
// --------------------
// 元编程判断类型列表中是否包含某个类型的辅助结构体
template <typename TypeList, typename T>
struct Contains;
// 特化:找到目标类型
template <typename T, typename... Ts>
struct Contains<TypeList<T, Ts...>, T> {
    static constexpr bool value = true;
};
// 递归:继续在剩余类型中查找
template <typename T, typename U, typename... Ts>
struct Contains<TypeList<U, Ts...>, T> {
    static constexpr bool value = Contains<TypeList<Ts...>, T>::value;
};
// 特化:未找到目标类型
template <typename T>
struct Contains<TypeList<>, T> {
    static constexpr bool value = false;
};
// --------------------
// 示例演示
// --------------------
int main() {
    // 定义一个类型列表
    using MyList = TypeList<int, float, double, char>;
    // 计算列表长度
    std::cout << "List length: " << Length<MyList>::value << std::endl;
    // 查找特定类型在列表中的位置
    std::cout << "Index of float: " << IndexOf<MyList, float>::value << std::endl;
    // 判断列表中是否包含某个类型
    std::cout << "Contains double? " << Contains<MyList, double>::value << std::endl;
    return 0;
}

这个示例展示了如何使用模板元编程来操作类型列表。通过定义 TypeList 结构体来表示类型列表,并使用递归和特化的方式实现不同操作。


在 main 函数中,我们定义了一个名为 MyList 的类型列表,其中包含了 int、float、double 和 char 四种不同类型。


接着,我们使用模板元编程实现了三个操作:计算列表长度、查找特定类型在列表中的位置、以及判断列表中是否包含某个类型。最后,在 main 函数中输出了计算结果。

4.3 C++代码一般示例

#include <iostream>
#include <vector>
#include <string>
// 声明所有可能的元素类型
struct Element1;
struct Element2;
struct Element3;
// 定义一个通用的访问器
template <typename... Types>
struct Visitor {
    virtual void visit(Types&...) = 0;
};
// 具体实现访问器的函数模板特化
template <typename T, typename... Types>
struct VisitorImpl : Visitor<Types...> {
    void visit(T& element, Types&... elements) override {
        std::cout << "Visiting element of type: " << typeid(T).name() << std::endl;
        // 在这里实现对特定类型元素的访问操作
        // ...
        Visitor<Types...>::visit(elements...);
    }
};
// 辅助函数模板用于创建访问器实例
template <typename... Types>
Visitor<Types...>* make_visitor() {
    return new VisitorImpl<Types...>();
}
int main() {
    // 创建一个通用的访问器实例
    Visitor<Element1, Element2, Element3>* visitor = make_visitor<Element1, Element2, Element3>();
    // 定义一些元素对象
    Element1 element1;
    Element2 element2;
    Element3 element3;
    // 对元素对象应用访问器
    visitor->visit(element1, element2, element3);
    // 释放访问器实例
    delete visitor;
    return 0;
}

这个示例中,我们定义了一个通用的访问器模板 Visitor,它接受一系列类型参数,并包含一个纯虚函数 visit。我们使用模板特化和继承的方式实现了访问器的具体行为。然后,我们提供了一个辅助函数模板 make_visitor,用于创建访问器实例。


在 main 函数中,我们创建了一个名为 visitor 的通用访问器实例,并将其指向具体的元素类型 Element1、Element2 和 Element3。然后,我们定义了一些元素对象,并通过访问器对其应用 visit 操作。


4.4 SFINAE(Substitution Failure Is Not An Error)

SFINAE(Substitution Failure Is Not An Error)是 C++ 模板元编程中的一项技术,它利用模板的重载解析规则来实现基于模板参数的特化选择。


在 C++ 中,当针对某个函数模板进行重载解析时,编译器会尝试对每个候选函数模板进行类型推导,并选择能够成功推导出参数类型的函数模板进行实例化。然而,当某个候选函数模板在实例化时无法通过模板参数进行合法的类型推导时,该函数模板不会被选择,而是被认为是解析失败。


SFINAE 技术就是利用这个行为,通过设计模板参数,使得某些特定情况下,某个函数模板的实例化会导致推导失败,从而导致编译器选择其他合法的函数模板。


这种技术的常见用途是实现模板函数的特化或重载,以处理不同的模板参数情况,从而在编译期间产生不同的行为。通过在函数模板候选函数中引入 SFINAE 的限制,可以实现根据不同模板参数类型选择不同的处理方式。


例如,可以使用 `std::enable_if` 结合类型判断表达式,限制在某些特定条件下才实例化特定的函数模板。当类型判断表达式返回 `true` 时,模板实例化成功,否则失败。这样可以实现在不同条件下使用不同实现的模板函数。


总而言之,SFINAE 技术是 C++ 模板元编程中的一种利用模板推导和重载解析规则的技术,能够根据模板参数情况选择特定的模板函数进行实例化。它对于实现条件化编译和选择性实例化非常有用,但也需要小心使用,以避免编译错误和代码可读性问题。

4.4.1 C++代码简单示例
#include <iostream>
#include <type_traits>
// 函数模板,根据类型参数是否为整数类型来选择实现
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
foo(T value) {
    std::cout << "foo() called for integral type: " << value << std::endl;
}
template <typename T>
typename std::enable_if<!std::is_integral<T>::value, void>::type
foo(T value) {
    std::cout << "foo() called for non-integral type: " << value << std::endl;
}
int main() {
    foo(42);  // 调用第一个 foo() 版本,参数为整数类型
    foo(3.14);  // 调用第二个 foo() 版本,参数为非整数类型
    return 0;
}

在这个示例中,我们定义了一个函数模板 foo,它根据类型参数 T 是否为整数类型来选择具体的实现。我们使用 std::enable_if 和 std::is_integral 类型 trait 进行条件判断,在编译期间决定是否实例化这两个版本的函数模板。


当类型参数 T 是整数类型时,std::enable_if<std::is_integral<T>::value, void>::type 的结果为 void,从而导致第一个版本的 foo 函数可用。而当类型参数 T 不是整数类型时,std::enable_if<!std::is_integral<T>::value, void>::type 的结果为 void,从而导致第二个版本的 foo 函数可用。


在 main 函数中,我们调用了两次 foo 函数,分别传递一个整数和一个浮点数。根据类型参数的不同,编译器会选择不同的 foo 版本进行实例化,并产生相应的输出。


这个示例展示了 SFINAE 技术通过使用 std::enable_if 结合类型 trait,实现了根据类型参数进行条件化的函数模板重载。这种技术可以用于实现更复杂的类型判断和条件化编译,以满足不同的需求。


总结

后续将会更新类模板以及更多符合C++设计哲学的知识讲解

目录
相关文章
|
1月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
244 65
|
1月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
108 4
|
30天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
16 1
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
46 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
25 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
42 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
37 4
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
34 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4