顺序、分支、循环代码的编写
顺序执行的代码
- 顺序计算类型的示例
template<typename T>
struct _RemoveRefConst{
private://声明为private确保函数的使用者不会误用
using inter_type = typename std::remove_reference<T>::type;//根据T算出inter_type
public:
using type = typename std::remove_const<inter_type>::type;//根据inter_type算出type
}
结构体中所有声明都要看成执行的语句,不能随意调换其顺序。
如果调整顺序程序将无法编译。(在编译期,编译器会扫描两遍结构体中的代码,一遍处理声明,一遍才深入定义)
- CRE:一个加法示例
template<int a, int b>
struct Sum
{
public:
static constexpr int result = a + b;
};
template<typename T>
struct Foo
{
public:
static constexpr int a = 1;
static constexpr int b = 2;
static constexpr int return_value = Sum<a, b>::result;
};
int main() {
std::cout << Foo<void>::return_value;//输出:3
return 0;
}
C++98/03标准只允许在类内部初始化const整型或枚举类型的静态成员变量。解决方法是在类外面进行定义和初始化。
C++11引入关键字constexpr,限定常量为编译期常量。
C++17引入的内联变量(inline variables),允许在类内部定义和初始化任何类型的静态成员变量,进一步简化代码。
- 补充
GPT:如果所有的值都是 constexpr,并且转换的类型也是编译时常量,那么 static_cast 可以在编译时执行。
分支执行的代码
编译期的分支逻辑既可以表现为存粹的元函数,也可以与运行期的执行逻辑相结合。
- 使用(部分)特化
可以使用(部分)特化来实现分支。(部分)特化天生就是引入差异的,使用它来实现分支也很自然。
- 使用
std::conditional
实现分支
namespace std{
//模板定义
template <bool b, typename Tt, typename Tf>
struct conditional{
using type = Tt;
}
//这也是部分特化
template<typename Tt, typename Tf>
struct conditional<false, Tt, Tf>{
using type = Tf;
}
}
CRE:用法类似运行期的
a?b:c
三目运算符。
只能实现二元分支,而不是实现类似switch的多元分支。
- 使用
std::enable_if
实现分支
//一个简单直观的示例:
// 仅当 T 是整型时启用此函数
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
sum(T a, T b)
{
return a + b;
}
int main() {
std::cout << sum(1, 2);
//std::cout << sum(1.0f, 2.0f);//IDE报错:没有与与参数列表匹配的 函数模板"sum"实例 参数列类型为(float, float)
return 0;
}
通常来说,
enable_if
会被用于函数之中,用作重载的有益补充–重载通过不同类型的参数来区别重名的函数。但在一些情况下,我们希望引入重名函数,但无法通过参数类型加以区分。此时通过enable_if
就能在一定程度上解决相应的重载问题。
GPT:当尝试用 float 类型特化这个函数模板时,
std::enable_if<std::is_integral<T>::value, int>::type
中的std::is_integral<float>::value
为false
。这会导致std::enable_if
没有定义type
,从而使函数特化失败。这是SFINAE(Substitution Failure Is Not An Error)原则的应用,即在模板实参替换过程中出现错误时,不会导致编译失败,而是选择其他可行的模板或报告未找到匹配的模板。
GPT:
enable_if
可以用于函数模板的返回值、形参、模板参数来体现SFINAE,其中作为返回值最常见。
- 编译期分支和多种返回类型
template<bool check, std::enable_if_t<check>* = nullptr>
auto fun(){
return (int)0;
}
template<bool check, std::enable_if_t<!check>* = nullptr>
auto fun() {
return (double)0;
}
template<bool check>
auto wrap() {
return fun<check>();
}
int main() {
std::cerr << wrap<true>() << std::endl;
return 0;
}
实现了一种编译期能够返回不同类型数据结果的函数。
C++14中,函数生命中可以不用显式指定其返回类型,编译期根据return语句自动推断返回类型。实际上不使用auto
关键字,改用enable_if
也可以实现。
- 使用
if constexpr
简化代码
在C++17中,可以把上面的代码简化为下面的代码。
template<bool check>
auto fun(){
if constexpr (check)
{
return (int)0;
}
else
{
return (double)0;
}
}
if constexpr
接收一个常量表达式,即编译期常量。编译期在解析到相关的函数调用时,会自动选择if constexpr
表达式为真的语句体,而忽略其他的语句体。
//当解析到:
std::cerr << fun<true>() << std::endl;
//编译器会自动构造如下函数:
// template<bool check>
auto fun(){
// if constexpr (check)
// {
return (int)0;
// }
// else
// {
// return (double)0;
// }
}
使用
if constexpr
写出的代码与运行期的分支代码更像。
但是if constexpr
使用场景较窄,它只能放在一般意义的函数内部。
循环执行的代码
为了能够有效地操作元数据,我们往往会使用递归的形式来实现循环。
一般来说,在采用递归实现循环的元程序中,需要引入一个分支来结束循环。
//用于计算一个二进制数的1的个数
template<size_t Input>
constexpr size_t OnesCount = (Input % 2) + OnesCount<(Input / 2)>;//递归
template<>
constexpr size_t OnesCount<0> = 0;//结束循环的分支
int main() {
std::cout << OnesCount<0b0101011>;//输出4
return 0;
}
小心实例化爆炸与编译崩溃
多重循环嵌套会产生大量的实例。
编译期的设计往往是为了满足一般性的编译任务,对于元编程这种目前来说情形并不多的技术来说,优化相对较少。因此编译器的开发者可能不会考虑编译过程中保存在内存的实例数过多的问题(对于非元编程的情况可能并不是大问题)。但另一方面,如果编译过程保存了大量实例,那么可能会导致编译器的内存超限,从而出现编译失败甚至崩溃。
分支选择和短路逻辑
减少编译期实例化的另一种重要方法就是引入短路逻辑。
CRE:短路也使用模板,用法参考?:
三目运算。
参考
《C++模板元编程实战》 – 李伟
(END)