C++基础与深度解析 | 元编程 | 元编程的编写方式 | 减少实例化技巧

发布于:2024-06-08 ⋅ 阅读:(112) ⋅ 点赞:(0)

这一章写的既浅又乱,为了知识的完整性先传上来,之后会重构

一、元编程的引入

  泛型编程提供了一种方式来编写通用的、类型无关的代码。然而,当需要针对某些特殊情况进行优化或引入额外的处理逻辑时,泛型编程可能就不够用了。这时,元编程可以作为一个补充,允许开发者在编译时根据类型或其他条件生成或选择代码。例如,你可以定义一个模板,它在编译时检查类型是否满足某个特性(如是否是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";
    }
    
  • 引入限定符防止误用

    使用限定符(如constvolatile)可以帮助防止代码的误用

  • 通过别名模板简化调用方式

    #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 */ {
      } 
      
    }
    #endif
    
    
    int 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 引入短路逻辑
  • 其他技巧介绍

    • 减少分摊复杂度的数组元素访问操作

网站公告

今日签到

点亮在社区的每一天
去签到