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月前
|
监控 Linux 测试技术
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
🌟 蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕C++与零拷贝网络编程,从sendfile到DPDK,实战优化服务器性能,毫秒级响应、CPU降60%。分享架构思维,共探代码星辰大海!
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
5月前
|
C语言 C++
【实战指南】 C/C++ 枚举转字符串实现
本文介绍了在C/C++中实现枚举转字符串的实用技巧,通过宏定义与统一管理枚举名,提升代码调试效率并减少维护错误。
371 58
|
5月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
204 46
|
9月前
|
监控 Linux C++
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。
348 86
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
|
9月前
|
人工智能 程序员 C++
【实战经验】C/C++右移高位补0还是1?
本文探讨了C/C++中右移运算时高位补0还是补1的问题。通过示例代码分析,揭示了右移规则:无符号类型高位补0;有符号类型根据正负决定(正数补0,负数补1)。文中列举了可能导致错误的场景,并提供了两种规避措施——使用无符号类型和掩码校正,确保结果符合预期。最后总结指出,右移运算虽常见,但若处理不当易引发隐晦Bug,需谨慎对待。
500 83
|
6月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
235 0
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
184 0
|
9月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
9月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
488 6