文章目录
- 一、元编程的引入
- 二、顺序、分支、循环代码的编写方式
- 1.顺序代码的编写方式
- 2.分支代码的编写方式
- 3.循环代码的编写方式
- 三、减少实例化的技巧
这一章写的既浅又乱,为了知识的完整性先传上来,之后会重构
一、元编程的引入
泛型编程提供了一种方式来编写通用的、类型无关的代码。然而,当需要针对某些特殊情况进行优化或引入额外的处理逻辑时,泛型编程可能就不够用了。这时,元编程可以作为一个补充,允许开发者在编译时根据类型或其他条件生成或选择代码。例如,你可以定义一个模板,它在编译时检查类型是否满足某个特性(如是否是POD类型),然后根据这个特性来选择不同的实现。
-
元编程与编译期计算
下面解释了如何使用编译期运算来辅助运行期计算:
-
编译期计算的优势
- 编译期计算可以在程序运行之前完成,这意味着不需要在程序执行时消耗计算资源。
- 编译期计算可以利用类型信息和模板参数来生成最优化的代码。
-
编译期与运行期的决策
- 需要仔细分析哪些计算可以在编译期完成,哪些必须在运行期进行。
- 编译期计算通常适用于那些不依赖于运行时数据的计算,如类型属性的计算、常量表达式的求值等。
-
编译期与运行期的结合
- 元编程不是简单地将计算一分为二,而是需要根据具体情况来决定哪些部分应该在编译期完成,哪些部分保留到运行期。
- 例如,如果一个算法的性能关键部分可以在编译期确定,那么这部分可以作为模板的一部分来实现。而对于依赖于运行时输入的部分,则需要在运行期处理。
-
运行期确定的信息
- 如果某种信息需要在运行期确定,比如用户输入或外部数据,那么这部分通常无法利用编译期计算。
- 但是,即使在这种情况下,也可以使用编译期计算来辅助运行期计算,比如通过模板元编程来生成运行期使用的代码或数据结构。
-
模板元编程示例
- 假设你有一个模板函数,它根据传入的类型参数来决定使用哪种算法。这种决策可以在编译期完成,而算法的具体实现则在运行期执行。
- 另一个例子是使用模板特化来实现类型特征的检测,如检查一个类型是否具有特定的成员函数或属性,然后在编译期根据这些特征生成相应的代码。
-
性能优化
- 通过将尽可能多的计算移至编译期,可以减少运行时的开销,提高程序的执行效率。
- 同时,编译期生成的代码可以针对特定情况进行优化,比如展开循环、消除冗余操作等。
-
编译期错误检查
编译期计算还可以用于错误检查和诊断,比如在编译时检测类型不匹配或参数错误,从而避免运行时错误。
-
元程序的形式:
C++中的元编程是一种在编译时执行代码生成和计算的技术。以下是C++元编程的一些主要形式:
-
模板
模板是C++中实现元编程的核心机制。通过模板,可以定义函数和类,它们可以处理任意类型或值。模板在编译时实例化,根据提供的类型或值参数生成具体的代码。
#include <iostream> #include <type_traits>template <typename T> void print_type_name() {std::cout << "Type: " << typeid(T).name() << std::endl; }int main() {print_type_name<int>(); // 编译时确定类型名称print_type_name<double>();return 0; }
-
constexpr函数
constexpr
函数是一种可以在编译时计算并返回常量表达式的函数。这些函数对于实现编译时计算非常有用,因为它们的结果可以被编译器优化和内联。#include <iostream>constexpr int add(int a, int b) {return a + b; }int main() {const int result = add(3, 5); // 编译时计算结果std::cout << "Result: " << result << std::endl;return 0; }
-
编译期可使用的函数
除了模板和
constexpr
函数,还有一些内建的编译期函数可以用于元编程,例如:-
sizeof
:返回一个类型或对象所占的字节数。int main() { return sizeof(int); // }
-
alignof
:返回类型所需的最小对齐字节数。
-
通常以函数为单位,也被称为函数式编程
元数据:(元程序的输入数据)
- 基本元数据:数值、类型、模板
- 数组
元程序的性质:
- 输入输出均为 “ 常量 ”
- 函数无副作用
type_traits元编程库
详细内容可参考:https://en.cppreference.com/w/cpp/header/type_traits
这里面的函数都是编译期可使用的函数
- C++11 引入到标准中,用于元编程的基本组件
二、顺序、分支、循环代码的编写方式
下面介绍元编程的编写方式。
1.顺序代码的编写方式
-
类型转换:去掉引用并添加
const
示例:
#include <iostream> #include <type_traits>template <typename T, unsigned S> struct Fun {using remRef = typename std::remove_reference<T>::type;using type = typename std::add_const<remRef>::type; };int main() {Fun<int&, 3>::type x = 3; //const int x = 3; }
-
代码无需至于函数中
通常置于模板中,以头文件的形式提供
-
更复杂的示例:
- 以数值、类型、模板作为输入
- 以数值、类型、模板作为输出
示例:
#include <iostream> #include <type_traits>template <typename T, unsigned S> struct Fun {using remRef = typename std::remove_reference<T>::type;constexpr static bool value = (sizeof(T) == 5);};int main() {constexpr bool res = Fun<int&, 4>::value;std::cout << res << "\n"; }
-
引入限定符防止误用
使用限定符(如
const
、volatile
)可以帮助防止代码的误用 -
通过别名模板简化调用方式
#include <iostream> #include <type_traits>template <typename T, unsigned S> struct Fun {using remRef = typename std::remove_reference<T>::type;constexpr static bool value = (sizeof(T) == 5);};//使用别名模版 template <typename T, int S> constexpr auto Fun_1 = Fun<T, S>::value;int main() {constexpr bool res = Fun_1<int&, 4>;std::cout << res << "\n"; }
以
std::is_same
举例,详细可参考:https://zh.cppreference.com/w/cpp/types/is_same。
2.分支代码的编写方式
下面介绍六种元编程分支代码的编写方式。
-
基于 if constexpr 的分支:便于理解只能处理数值,同时要小心引入运行期计算
if constexpr
是C++17引入的特性,它允许在编译时根据条件编译不同的代码分支。这使得模板代码更加灵活。#include <iostream>template <typename T> void process(T value) {if constexpr (std::is_integral<T>::value) {// 仅当T是整数类型时编译此分支std::cout << "Integral type" << std::endl;} else {// 其他类型std::cout << "Non-integral type" << std::endl;} } int main() {process<int>(100); //Integral type }
经编译后的代码为:
#include <iostream>template<typename T> void process(T value) {if constexpr(std::is_integral<T>::value) {std::operator<<(std::cout, "Integral type").operator<<(std::endl);} else /* constexpr */ {std::operator<<(std::cout, "Non-integral type").operator<<(std::endl);} }/* First instantiated from: insights.cpp:16 */ #ifdef INSIGHTS_USE_TEMPLATE template<> void process<int>(int value) {if constexpr(true) {std::operator<<(std::cout, "Integral type").operator<<(std::endl);} else /* constexpr */ {} } #endifint main() {process<int>(100);return 0; }
-
基于(偏)特化引入分支:常见分支引入方式但书写麻烦
模板特化可以用来为特定的类型或值提供定制化的实现,这是一种常见的分支引入方式。
#include <iostream>template <typename T> struct Processor;// 对于其他类型,使用默认实现 template <typename T> struct Processor {void process(T value) {// 默认实现std::cout << "process(T value)" << std::endl;} };template <> struct Processor<int> {void process(int value) {// 针对int的特化实现std::cout << "process(int value)" << std::endl;} };int main() {Processor<int> p;p.process(100); //process(int value)Processor<double> p1;p1.process(3.14); //process(T value) }
-
基于 std::conditional 引入分支:语法简单但应用场景受限
详细内容可参考:https://en.cppreference.com/w/cpp/types/conditional
若
B
在编译时为 true 则定义为T
,或若B
为 false 则定义为F
。std::conditional
是一个编译时条件选择器,可以用来基于模板参数选择不同的类型。#include <iostream> #include <type_traits> #include <typeinfo>int main() {using Type1 = std::conditional<true, int, double>::type;using Type2 = std::conditional<false, int, double>::type;using Type3 = std::conditional<sizeof(int) >= sizeof(double), int, double>::type;std::cout << typeid(Type1).name() << '\n';std::cout << typeid(Type2).name() << '\n';std::cout << typeid(Type3).name() << '\n'; }
运行结果:
i d d
-
基于 SFINAE 引入分支
-
基于 std::enable_if 引入分支:语法不易懂但功能强大
详细内容可参考:https://zh.cppreference.com/w/cpp/types/enable_if
std::enable_if
可以用来根据条件启用或禁用模板实例化。#include <iostream>template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>> //为true时等于void void process_integral(T value) {// 仅当T是整数类型时启用此函数std::cout << "111" << std::endl; }int main() {process_integral<int>(100); }
注意用做缺省模板实参不能引入分支!
-
基于 std::void_t 引入分支: C++17 中的新方法,通过 “无效语句” 触发分支
std::void_t
是C++17引入的,可以用来在SFINAE中创建一个空类型,从而在条件不满足时使模板实例化失败。
-
-
基于 concept 引入分支: C++20 中的方法
可用于替换 enable_if
template <typename T> concept Integral = std::is_integral<T>::value;template <Integral T> void process(T value) {// 仅当T满足Integral概念时编译此分支 }
-
基于三元运算符引入分支: std::conditional 的数值版本
#include <iostream> #include <type_traits>template <int x> constexpr auto fun = (x < 100) ? x*2 : x-3;constexpr auto x = fun<102>();int main() {std::cout << x << std::endl; }
3.循环代码的编写方式
C++元编程中的循环代码编写方式,这是一种在编译时执行循环的技术。
-
简单的示例:计算二进制中包含 1 的个数
#include <iostream> #include <type_traits>template <int x> constexpr auto fun = (x % 2) + fun<x / 2>;template<> constexpr auto fun<0> = 0;constexpr auto x = fun<99>;int main() {std::cout << x << std::endl; }
经编译器处理可得到
#include <iostream> #include <type_traits>template<int x> constexpr const auto fun = (x % 2) + fun<x / 2>;template<> constexpr const int fun<99> = (99 % 2) + fun<49>; template<> constexpr const int fun<49> = (49 % 2) + fun<24>; template<> constexpr const int fun<24> = (24 % 2) + fun<12>; template<> constexpr const int fun<12> = (12 % 2) + fun<6>; template<> constexpr const int fun<6> = (6 % 2) + fun<3>; template<> constexpr const int fun<3> = (3 % 2) + fun<1>; template<> constexpr const int fun<1> = (1 % 2) + fun<0>;template<> constexpr const int fun<0> = 0;constexpr const int x = fun<99>;int main() {std::cout.operator<<(x).operator<<(std::endl);return 0; }
-
在编译期通常会使用递归来实现循环
-
任何一种分支代码的编写方式都对应相应的循环代码编写方式
三、减少实例化的技巧
-
为什么要减少实例化
- 提升编译速度,减少编译所需内存
-
相关技巧
- 提取重复逻辑以减少实例个数
- conditional使用时避免实例化
- 使用std::conjunction / std::disjunction 引入短路逻辑
-
其他技巧介绍
- 减少分摊复杂度的数组元素访问操作