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

本文涉及的产品
云解析DNS-重点域名监控,免费拨测 20万次(价值200元)
简介: 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++设计哲学的知识讲解

目录
相关文章
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
6月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
218 0
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
174 0
|
10月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
9月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
9月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
478 6
|
10月前
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
152 0
|
10月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
10月前
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。