C++模板元模板实战书籍讲解第一章(元函数与type_traits)

简介: C++模板元模板实战书籍讲解第一章(元函数与type_traits)

前言

一个深度学习框架的初步实现为例,讨论如何在一个相对较大的项目中深入应用元编程,为系统优化提供更多的可能。


以下是本书的原文《C++模板元编程实战》,由李伟先生所著写。


百度网盘链接:


链接:https://pan.baidu.com/s/1e4QIRSDEfCR7_XK6-j-19w

提取码:57GP


提示:以下是本篇文章正文内容,下面内容主要为个人理解以及少部分正文内容

一、元函数介绍

元函数(Metafunction)是一种在编译时进行计算的模板元编程技术,它可以根据类型参数的信息生成结果类型。元函数可以被用于定义通用的、在编译时计算的类型转换、条件判断、类型推导等需求。


在C++中,元函数主要基于模板元编程(Template Metaprogramming)技术实现。它通过模板特化和递归定义,根据输入的类型参数,在编译期进行类型计算和操作。


元函数可以根据不同的类型参数,生成不同的结果类型。这使得我们可以在编译时根据不同的类型特性动态地生成代码,提高代码的可复用性和灵活性。

constexpr int square(int n) {
    return n * n;
}
int main() {
    constexpr int result = square(5);  // 计算 5 的平方
    // result = 25
    return 0;
}

其中constexpr关键字为C++11的关键字,可以在编译期被调用!具体看我这一期

带你了解并掌握一些C++关键字的使用-CSDN博客

static int call_cout = 3; 
constexpr int fun2(int a) 
{ 
    return a + (call_cout++); 
}

这个程序片段是无法通过编译的,它是错误的。


fun2() 函数尝试在编译期间对 call_cout 进行自增操作。然而,自增操作是一个副作用,它会修改变量的值。由于 call_cout 是一个非常量全局变量,并且 fun2() 尝试修改它的值,这违反了 constexpr 函数不能有副作用的规定。


为了让 fun2() 成为一个合法的 constexpr 函数,你需要确保函数体内没有副作用的操作。换句话说,函数体中的计算只能依赖于其输入参数,并且不能修改状态或执行那些不能在编译期间确定结果的操作。


如果你需要在 fun2() 中使用可变状态,那么你应该考虑将其实现为一个运行时函数,而不是使用 constexpr 关键字。运行时函数可以在程序运行时执行,允许进行副作用操作和状态修改。


以上只是C++涉及到的一种元函数,事实上C++用的更多的是类型元函数-即以类型作为输入和输出的元函数。


二、类型元函数

从数学的角度来看,函数通常可以被写成 y = f(x) 的形式,其中 x 是自变量,y 是因变量,而 f(x) 则表示自变量 x 经过函数 f 的变换所得到的结果。这种表示方式的主要原因如下:


1. 显式展示变量关系:通过使用 y = f(x) 的形式,我们能够直观地看到自变量 x 和因变量 y 之间的关系。这种形式清晰地表达了每个输入值 x 对应的输出值 y。


2. 函数定义的输入输出关系:函数是一种映射关系,它将自变量 x 映射到因变量 y。通过使用 y = f(x) 的形式,我们能够明确说明函数 f 的定义域(输入)和值域(输出)之间的映射关系。


3. 支持通过 x 和 y 之间的关系进行推理和操作:使用 y = f(x) 的形式,我们可以进行函数的运算和分析。我们可以通过给定 x 的值来计算相应的 y 值,也可以反过来,通过给定 y 的值,求解满足条件的 x 值。


4. 便于比较和组合函数:使用 y = f(x) 的形式,我们可以轻松地比较和组合多个函数。通过对比不同函数的表达式 f(x) 和 g(x),我们可以分析它们的性质并进行比较。而且,可以将一个函数的输出作为另一个函数的输入,从而实现函数的组合。


总的来说,使用 y = f(x) 的形式能够提供一种简洁、直观和通用的方式来表示函数的定义和变换关系,方便了数学的表达和推理。


在数学和计算机科学中,类型元函数(type-level function)是指一种能够在类型级别上操作的函数。它与普通函数(值级别函数)类似,但是在类型系统中起作用,并且操作的是类型而不是具体的值。


类型元函数是一种在类型级别上进行操作和转换的工具,它可以接受类型作为输入,并生成新的类型作为输出。它可以用于类型推断、类型转换、类型约束和类型操作等各种编程和数学任务。


在编程语言中,特别是静态类型语言中,类型元函数可以用于定义和处理复杂的类型系统。它们可以帮助程序员在编译时进行类型检查和验证,提供更强大的类型推断和静态分析能力。


例如,Haskell语言中的类型类(type class)和类型族(type family)就是类型元函数的一种实现方式。类型类定义了一组相关类型的共享行为和操作,而类型族则允许基于输入类型生成输出类型。

#include <iostream>
#include <type_traits>
// 定义类型元函数,判断一个类型是否为整数类型
template <typename T>
struct IsInteger {
    static constexpr bool value = std::is_integral_v<T>;
};
int main() {
    int num = 42;
    double pi = 3.14;
    std::string name = "WeTab";
    std::cout << "IsInteger<int>: " << IsInteger<decltype(num)>::value << std::endl;               // 输出 1
    std::cout << "IsInteger<double>: " << IsInteger<decltype(pi)>::value << std::endl;           // 输出 0
    std::cout << "IsInteger<std::string>: " << IsInteger<decltype(name)>::value << std::endl;  // 输出 0
    return 0;
}

在这个示例代码中,`IsInteger` 是一个用于判断类型是否是整数类型的类型元函数模板。对于每个传入的类型 `T`,`std::is_integral_v<T>` 会返回一个布尔值,表示该类型是否是整数类型。我们使用 `decltype` 获取变量的类型,然后通过 `IsInteger<decltype(num)>::value` 来获取判断结果。


其中 `std::is_integral_v<T>` 是模板 `std::is_integral` 的C++17中引入的变体,用于更方便地获取类型是否是整数类型的信息。


元函数的定义通常包括以下几个关键要素:


  1. 使用模板:元函数通常以模板的形式定义,即使用 template 关键字来声明模板参数。
  2. 模板参数:元函数通过模板参数来接受输入类型。可以使用一个或多个模板参数,并且可以使用类型、非类型和模板模板参数。
  3. 类型操作:元函数在模板定义的函数体内,对传入的类型进行操作、推断和转换。可以使用类型特征、类型转换、类型定义和类型运算等来实现所需的操作。
  4. 类型别名或成员常量:元函数通常定义一个类型别名或成员常量,以便在编译时可以访问计算的结果。

三、各式各样的元函数

下面是两个无参元函数的示例,一个用于返回类型,一个用于返回值:

#include <iostream>
#include <type_traits>
// 元函数:返回类型
template <typename T>
struct ReturnTypeName {
    using type = T;
};
int main() {
    // 使用元函数获取返回类型
    typename ReturnTypeName<int>::type num;
    std::cout << "Type of num: " << typeid(num).name() << std::endl;           // 获取 num 的类型
    std::cout << "Type of ReturnTypeName<int>::type: " << typeid(num).name() << std::endl;    // 获取 ReturnTypeName<int>::type 的类型
    return 0;
}

在上述示例代码中,`ReturnTypeName` 是一个元函数,它接受一个模板参数 `T`,并定义了一个类型别名 `type`,用于存储返回的类型。在 `main` 函数中,我们使用这个元函数将 `int` 类型作为模板参数,获取了类型别名 `ReturnTypeName<int>::type` 的返回类型,并将其存储到 `num` 变量中。使用 `typeid` 来获取变量 `num` 和 `ReturnTypeName<int>::type` 的类型。

下面是另一个无参元函数的示例,用于返回值:

#include <iostream>
// 元函数:返回值
template <typename T>
struct ReturnValue {
    static constexpr T value = 42;
};
int main() {
    // 使用元函数获取返回值
    constexpr int result = ReturnValue<int>::value;
    std::cout << "Value return by ReturnValue<int>: " << result << std::endl;
    return 0;
}

在这个示例代码中,`ReturnValue` 是一个元函数,它接受一个模板参数 `T`,并定义了一个静态常量 `value`,用于存储返回的值。在 `main` 函数中,我们使用这个元函数将 `int` 类型作为模板参数,获取了静态常量 `ReturnValue<int>::value` 的返回值,并将其存储到 `result` 变量中。最后,输出 `result` 的值。

template <int a>
constexpr int fun = a + 1;

为啥说这也是元函数?

使用实例:

#include <iostream>
template <int a>
constexpr int fun = a + 1;
int main() {
    constexpr int result = fun<5>;
    std::cout << "Result: " << result << std::endl;
    return 0;
}

这个示例中,我们通过调用 fun<5> 来实例化函数模板,并将结果赋值给 result 变量。然后,将 result 的值输出到控制台。根据输入输出的映射关系,fun<5> 将被编译时计算为 5 + 1,结果为 6。因此,输出结果将为 6。


需要注意的是,元函数并不一定需要返回类型或值,它的主要特征在于在编译时对类型或计算进行操作和推断。


四、type_traits

type_traits 元函数库是 C++ 标准库中的一个头文件 `<type_traits>`,它提供了一组元函数模板,用于在编译时对类型进行类型特性的查询和操作。这个库使得开发者能够在编译时进行类型相关的判断和操作,从而实现更加灵活和通用的代码编写。


type_traits 元函数库提供了以下几个常用的元函数模板:

1. `std::is_same<T, U>`:                用于判断类型 `T` 和类型 `U` 是否相同,如果是返回 `true`,否则返回 `false`。
2. `std::is_integral<T>`:               用于判断类型 `T` 是否为整型,如果是返回 `true`,否则返回 `false`。
3. `std::is_floating_point<T>`:         用于判断类型 `T` 是否为浮点型,如果是返回 `true`,否则返回 `false`。
4. `std::is_pointer<T>`:                用于判断类型 `T` 是否为指针类型,如果是返回 `true`,否则返回 `false`。
5. `std::is_array<T>`:                  用于判断类型 `T` 是否为数组类型,如果是返回 `true`,否则返回 `false`。
6. `std::conditional<Condition, T, U>`: 根据条件 `Condition`,在类型 `T` 和类型 `U` 中选择一个作为返回类型。
7. `std::remove_const<T>`:              从类型 `T` 中移除 `const` 修饰符,返回结果类型。
8. `std::add_pointer<T>`:               给类型 `T` 添加一个指针修饰符,返回指针类型。
9. `std::enable_if<Condition, T>`:      在满足条件 `Condition` 为真时,定义一个类型 `T`,否则不定义。

这些元函数模板允许在编译时根据类型特性进行条件判断、类型转换和类型推导等操作,极大地增强了 C++ 语言的灵活性和表达能力。

使用 type_traits 元函数库需要包含头文件 `<type_traits>`,然后根据需要选择合适的元函数模板进行使用。

4.1 std::remove_reference<T>

`std::remove_reference<T>` 是 type_traits 元函数库中的一个元函数模板,用于从类型 `T` 中移除引用修饰符。它返回一个新类型,该新类型是从 `T` 移除了引用修饰符的版本。


用法如下:

#include <iostream>
#include <type_traits>
int main() {
    int x = 42;
    int& rx = x;
    std::remove_reference<decltype(rx)>::type y = 99;
    std::cout << "y: " << y << std::endl;   // 输出 y: 99
    return 0;
}

在上面的示例代码中,我们定义了一个变量 `x`,并用 `int&` 创建了一个引用变量 `rx`。然后,我们使用 decltype 关键字获取 `rx` 的类型,并通过 `std::remove_reference` 元函数模板获得它的非引用版本,即 `int`。最后,我们使用 `int` 类型的变量 `y` 来存储一个整数值,并输出它的值。


需要注意的是,`std::remove_reference<T>::type` 是一个用于获取移除了引用修饰符的类型的别名。在上述示例中,`std::remove_reference<decltype(rx)>::type` 将被推导为 `int` 类型。

4.2 std::remove_reference_t<T>

`std::remove_reference_t<T>` 是 type_traits 元函数库中的一个类型别名模板,用于从类型 `T` 中移除引用修饰符,并直接获得移除引用后的类型。

用法如下:

#include <iostream>
#include <type_traits>
int main() {
    int x = 42;
    int& rx = x;
    std::remove_reference_t<decltype(rx)> y = 99;
    std::cout << "y: " << y << std::endl;   // 输出 y: 99
    return 0;
}

在上面的示例代码中,我们定义了一个变量 `x`,并用 `int&` 创建了一个引用变量 `rx`。然后,我们使用 decltype 关键字获取 `rx` 的类型,并通过 `std::remove_reference_t` 类型别名模板获得它的非引用版本,即 `int`。最后,我们使用 `int` 类型的变量 `y` 来存储一个整数值,并输出它的值。


`std::remove_reference_t<T>` 是 C++14 引入的类型别名模板,它直接返回移除引用修饰符后的类型,省去了使用 `::type` 获取类型的步骤,使代码更加简洁和直观。


使用 `std::remove_reference_t` 可以方便地获得移除引用修饰符后的类型,并在编写代码时避免引用相关的类型操作和模板特例化。

五、元函数与宏

它们之间的一些关系:

  1. 功能:元函数是在编译时进行类型查询和操作的函数模板,而宏是在预编译阶段进行文本替换和代码转换的预处理指令。元函数主要用于类型相关的操作和判断,而宏主要用于文本替换和代码生成。
  2. 类型安全:元函数在编译时执行类型检查,因此可以提供更好的类型安全性和错误检查。而宏在预处理阶只进行简单的文本替换,不进行类型检查,容易引入潜在的类型错误。

  3. 可读性:元函数使用标准的C++语法进行类型操作,可以提供更加清晰和可读的代码。宏使用宏语法和额外的符号替换规则,可能会使代码变得晦涩难懂。
  4. 执行时机:元函数是在编译时展开和执行的,因此可以提供更高的性能和效率。而宏是在预处理阶段替换代码,会增加编译时间和生成更多的中间代码。

以下是书中原图:

六、书中元函数的命名方式


总结

总而言之,元函数是在编译期间对类型进行操作和计算的模板函数,它们利用编译器的模板元编程能力来实现类型转换、判断和计算。通过使用递归、模板特化和类型 trait,元函数能够处理复杂的类型结构和条件情况,并提供编译期间的灵活性和性能优化。元函数在实现类型 trait、条件化编译和元编程库中都扮演着重要的角色,为 C++ 提供了更高层次的抽象和编程能力。

目录
相关文章
|
2天前
|
存储 编译器 C++
|
4天前
|
C++ 编译器 程序员
C++ 从零基础到入门(3)—— 函数基础知识
C++ 从零基础到入门(3)—— 函数基础知识
|
4天前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
36 1
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
|
4天前
|
存储 算法 编译器
C++的模板与泛型编程探秘
C++的模板与泛型编程探秘
11 0
|
4天前
|
编译器 C++
【C++从练气到飞升】08---模板
【C++从练气到飞升】08---模板
|
4天前
|
存储 算法 对象存储
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
16 1
|
4天前
|
算法 编译器 C++
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
23 1
|
4天前
|
存储 算法 数据安全/隐私保护
【C++入门到精通】 哈希结构 | 哈希冲突 | 哈希函数 | 闭散列 | 开散列 [ C++入门 ]
【C++入门到精通】 哈希结构 | 哈希冲突 | 哈希函数 | 闭散列 | 开散列 [ C++入门 ]
7 0
|
4天前
|
存储 自然语言处理 C++
刷题用到的非常有用的函数c++(持续更新)
刷题用到的非常有用的函数c++(持续更新)
19 1
|
4天前
|
编译器 C语言 C++
【C++】模板进阶
【C++】模板进阶
14 1