前言
一个深度学习框架的初步实现为例,讨论如何在一个相对较大的项目中深入应用元编程,为系统优化提供更多的可能。
以下是本书的原文《C++模板元编程实战》,由李伟先生所著写。
百度网盘链接:
链接:https://pan.baidu.com/s/1e4QIRSDEfCR7_XK6-j-19w
提取码:57GP
一、顺序执行的代码?
template <typename T> struct RemoveReferenceConst_ { private: using inter_type = typename std::remove_reference<T>::type; public: using type = typename std::remove_const<inter_type>::type; }; template <typename T> using RemoveReferenceConst = typename RemoveferenceConst_<T>::type RemoveReferenceConst<const int&> h = 3;
我们来逐行解释代码:
template <typename T> struct RemoveReferenceConst_ { using inter_type = typename std::remove_reference<T>::type; using type = typename std::remove_const<inter_type>::type; };
RemoveReferenceConst_ 是一个模板结构体,接受一个类型 T 作为模板参数。它定义了一个私有成员类型 inter_type,它使用 std::remove_reference 获取 T 的非引用类型。然后,它定义了一个公有成员类型 type,它使用 std::remove_const 获取 inter_type 的非常量类型。综合起来,RemoveReferenceConst_ 结构体将从 T 中去除参考和常量修饰符。
接下来,我们来解释第二个模板别名的定义:
template <typename T> using RemoveReferenceConst = typename RemoveReferenceConst_<T>::type;
RemoveReferenceConst 是一个模板别名,它接受一个类型 T 作为模板参数。
它使用 RemoveReferenceConst_ 结构体,获取其内部的 type 成员类型。
因此,RemoveReferenceConst 提供了一个简单的方式,通过调用 RemoveReferenceConst_ 结构体,去除 T 的参考和常量修饰符。
最后,我们来看看代码中的使用示例:
RemoveReferenceConst<const int&> h = 3;
这里使用 RemoveReferenceConst 模板别名,将 const int& 类型去除了引用和常量修饰符。因此,h 的类型将为 int。并且,通过将整数值 3 赋给 h,即可初始化并赋值给 h。
综上所述,这些代码提供了一种去除类型引用和去除类型常量修饰符的机制,并通过示例展示了如何使用它。
1.1 定义顺序调换?
struct RunTimeExample{ static void fun1() { fun2(); } static void fun2() { cerr << "hello" << endl; } };
结构体中的成员函数实际上是静态函数,可以通过结构体名字加上作用域解析运算符(::)调用,而不是通过结构体的实例来调用。因此,结构体中函数的定义顺序在编译时并不会影响它们的可见性或调用关系。
无论在结构体中的哪个位置定义函数,编译器都能正确地解析和调用这些函数。这是因为在编译时,编译器会对整个结构体的定义进行解析,了解每个函数的存在,并将其合适地放入符号表中。
template <typename T> struct RemoveReferenceConst_ { using type = typename std::remove_const<inter_type>::type; using inter_type = typename std::remove_reference<T>::type; }:
ype 和 inter_type 是类型别名,它们是在模板实例化时才被推导和使用的。在该代码片段中,type 的定义在 inter_type 之前,这违反了类型依赖的编译规则。
在 C++ 中,类型的定义是按照源代码顺序进行解析的。当编译器在解析 type 的定义时,它会试图去解析 inter_type,但是在这个阶段 inter_type 还没有被定义。因此,编译器会报错。
二、分支执行的代码
编译期和运行期分支的不同,以及是如何配合的
编译期分支是指在编译时期根据条件选择性地包含或排除一部分代码,以在运行时执行不同的逻辑。这种条件执行是在编译时通过预处理指令、模板特化或宏展开等机制来决定的。在编译期分支中,根据条件的不同,编译器会根据用户提供的代码,生成不同的目标代码,从而在运行时执行相应的功能。
运行期分支是指在程序运行时根据条件选择性地执行不同的代码路径。这种条件执行是通过 if 语句、switch 语句等控制结构来实现的。在运行期分支中,程序在运行时会根据条件的判断结果,选择执行特定的代码块,从而实现不同的逻辑路径。
编译期分支主要用于在编译时对程序进行静态优化,根据不同的条件生成不同的代码,从而提高程序的性能。这种分支在编译时确定,通过代码替换、剪枝等方式进行优化。
运行期分支则更加灵活,可以根据运行时的具体情况来选择执行逻辑,使程序具有更大的可变性和适应性。
两种分支逻辑结合的方式主要依赖于具体的编程语言和工具链。例如,在一些编程语言中,可以使用条件编译指令(例如 #if、#ifdef 等)来在编译时选择不同的代码路径。而在其他情况下,可以通过使用模板、宏等特性来实现编译期分支。在运行时,可以使用条件语句(如 if、switch)来进行运行期的分支判断。
2.1 使用 std::conditional 与 std::conditional_t 实现分支
namespace std { template <bool B, typename T, typename F> struct conditional{ using type = T; }; tyemplate <typename T, typename F> struct conditional<false, T, F>{ using type = F; }; template <bool B, typename T, typename F> using conditional_t = typename conditional<B, T, F>::type; }
其逻辑行为是:如果 B 为真, 则函数返回 T,否则返回 F。其典型的使用方式为
std::conditional<true, int, float>::type x = 3; std::conditional_t<flase, int, float> y = 1.0f;
代码较为简单,不做详细讲解,如下图原文
2.2 使用(部分)特化实现分支
示例代码
#include <iostream> template <typename T> struct BranchLogic { static void DoBranch() { std::cout << "Default branch" << std::endl; } }; template <> struct BranchLogic<int> { static void DoBranch() { std::cout << "Int branch" << std::endl; } }; template <> struct BranchLogic<char> { static void DoBranch() { std::cout << "Char branch" << std::endl; } }; int main() { BranchLogic<double>::DoBranch(); // 输出:Default branch BranchLogic<int>::DoBranch(); // 输出:Int branch BranchLogic<char>::DoBranch(); // 输出:Char branch return 0; }
在上述示例中,我们定义了一个 BranchLogic 的模板结构体,它具有一个静态成员函数 DoBranch()。默认情况下,DoBranch() 函数会输出 “Default branch”。但是通过对 BranchLogic 进行部分特化,我们可以根据类型参数的不同来选择不同的实现。
在主函数中,我们分别调用了 BranchLogic<double>::DoBranch()、BranchLogic<int>::DoBranch() 和 BranchLogic<char>::DoBranch()。根据类型参数的不同,对应的特化版本会被实例化,从而输出不同的结果。
注意点
template <typename TW> struct Wrapper{ template <typename T> struct Fun_ { constexpr static size_t value = 0; }; template<> struct Fun_<int> { constexpr static size_t value = 1; }; };
Wrapper是一个未完全特化的类模板,但在其内部包含了一个模板的完全特化Fun_<int>,这是C++标准不允许的,会产生编译错误。
原因
在C++标准中,一个类模板的成员模板可以进一步进行特化,也可以保持未完全特化的状态。然而,如果一个类模板有一个未完全特化的成员模板,并且同时包含该成员模板的完全特化,可能会导致歧义和冲突的情况发生。
具体来说,以下是可能导致歧义和冲突的情况:
- 重复定义的问题:如果在类模板中既有未完全特化的成员模板,又有该成员模板的完全特化,那么编译器将无法确定如何处理这种重复定义的情况。
- 成员模板选择的问题:在使用该类模板的时候,如果要实例化成员模板,编译器无法确定应该选择哪个版本(未完全特化还是完全特化),因为两者的定义可能会相互冲突。
为了避免这些歧义和冲突,C++标准规定了一个类模板不允许同时包含未完全特化的成员模板和该成员模板的完全特化。
std::cout << Wrapper<double>::Fun_<float>::value << std::endl; // 输出:0 // 编译错误,同时包含了未完全特化和完全特化的成员模板 std::cout << Wrapper<int>::Fun_<int>::value << std::endl;
修改示例如下书中原图
2.3 使用 std::enable_if 与 std::enable_if_t 实现分支
以下为书中原图
2.3.1 SFINAE特性
在C++中,SFINAE(Substitution Failure Is Not An Error)是一种编译器特性,它允许在模板推断和重载解析失败时,从候选函数集合中剔除导致错误的函数或模板。
SFINAE的主要思想是,当编译器在进行模板实例化或函数重载解析时,如果在推断类型、实例化模板、计算表达式等过程中出现了错误,编译器不会报错,而是将错误的候选项从候选函数集合中排除,继续查找其他可行的候选项。这样的行为使得程序可以根据条件选择性地启用或禁用特定的重载或模板。
SFINAE通常与模板元编程结合使用,用于在编译期间根据类型特征、条件判断等进行编译时的逻辑分支。
#include <iostream> #include <type_traits> // 定义一个模板类和模板函数 template<typename T> struct HasMemberFunction_foo { // 使用SFINAE进行模板特化 template<typename U> static std::true_type test( decltype(std::declval<U>().foo(), void()), int); template<typename U> static std::false_type test(...); // 使用类型别名检查函数返回值 constexpr static bool value = decltype(test<T>(0))::value; }; // 定义一个类 struct MyClass { void foo() {} }; int main() { std::cout << HasMemberFunction_foo<MyClass>::value << std::endl; // 输出:1 std::cout << HasMemberFunction_foo<int>::value << std::endl; // 输出:0 return 0; }
如上示例,我们定义了一个模板类 `HasMemberFunction_foo`,该类用于检查类型 `T` 是否具有名为 `foo` 的成员函数。
在 `HasMemberFunction_foo` 中,我们使用SFINAE技术进行模板特化。我们定义了两个静态成员函数 `test`,一个函数使用了表达式SFINAE,它检查表达式 `std::declval<U>().foo()` 是否可以成功推断为 `void` 类型并返回了 `decltype` 的结果;另一个函数是一个通配符函数,它接受任意类型的参数。
根据SFINAE规则,如果 `std::declval<U>().foo()` 的推断成功,那么会选择第一个函数;否则,会选择第二个函数。
然后,我们使用类型别名 `value` 来获取 `test` 的返回值,即 `std::true_type` 或 `std::false_type`,并输出结果。
2.4 编译期分支与多种选择类型
如下内容原文内容言简意赅,所以请看如下书中原图
尽管在C++14中可以使用auto来推断函数的返回类型,但是这种推断仍然遵循一些限制和规则。
首先,对于具有多个返回语句的函数,这些返回语句必须具有相同的类型,以满足C++中函数必须具有唯一的返回类型的要求。编译器将使用最常见的类型作为推断结果,如果这些类型不同或不能隐式转换,则会导致编译错误。
其次,如果函数体中没有返回语句,编译器将无法推断返回类型,这也会导致编译错误。因此,函数必须具有完整的函数体,或者使用其他方法来指定返回类型。
最后,推断的返回类型不能是引用类型,即不能使用auto&或auto&&来推断返回类型。如果需要返回引用类型,仍然需要显式指定返回类型。