现代C++学习笔记(8):别名与代码化简
Image (题图来自网络并做适当修改)
1
这一篇,重新回到《Effective Modern C++》的学习,进入“条款3:理解decltype”。这个条款的重点在介绍 decltype 这个关键词,尤其是与之相关的型别推导。
该关键词在 C++11 中首次登场,一眼看去似乎非常简单,简单到让人觉得甚至可能都没有存在的价值。然而,它其实是个超级难以琢磨的存在,以至于在“条款3”的开篇就直言“说起 decltype,这是个古灵精怪的东西”,而在后面,甚至坦然说道“想要彻底理解 decltype 的行为,就需要熟悉若干特殊情况。其中的大部分都太过艰涩,在本书中也不适合完全展开”。
所以,本文决定仅从其一个侧面来进行笔记:对代码的化简。虽然这未必是 decltype 关键词引入的初衷,但却是一个可以切入并逐步深入理解的角度。
2
说到代码简化,这是编程中永恒的话题和追求。在《重构》一书中,重复的代码被称为“有臭味的代码”,因为它会导致各种各样让代码难以维护的问题,所以值得花大量时间,对代码进行“除臭”,即减少重复。
早在 C 语言的时代,作为编译器之外提供的附加功能,“预编译”就祭出了“宏”这样一个伟大的发明。把重复的代码通过宏加以定义,并允许使用参数,对宏展开的代码文本进行部分替换。这是个非常有效的做法,甚至可以说“如洪荒之力般强大”,以至于时至今日仍被广泛使用着。一个著名的例子是两个数的比较,并从中选取最小值或最大值:
#define min(a, b) (a < b ? a : b) #define max(a, b) (a > b ? a : b) 上面的例子对于简单的数字或单个变量是很好用的,避免了每次都写冗长的“… ? … : …”三目运算符。然而,如果传入 min 或 max 的参数是一个相对复杂的表达式,则因为运算符之间的优先顺序,则可能导致最终程序逻辑完全不同于预想。所以,通常会给宏定义的变量加上括号,以确保编译器按照预期工作:
#define min(a, b) ((a) < (b) ? (a) : (b)) #define max(a, b) ((a) > (b) ? (a) : (b)) 但如果表达式中带有自增或自减等运算,或其他可能带有“副作用”的函数调用,则情况会变得非常难以预料。例如:
int a = 1, b = 2; int c = min(a++, b++); std::cout « a « “, " « b « “, " « c « std::endl; 上面的例子中,a、b、c 三个变量的结果最终如何,各编译器未必会给出同样的结果。这属于标准上没有严格说明的“未定义行为”。从代码的书写者角度看,a 和 b 应该只自增一次。但由于宏定义本身,导致该“a++”与“b++”代码被展开了多次,也即最终编译生成的代码,会发生多次自增,而且扩展的这些内容,执行的先后顺序也并不确定(缺乏规范标准来要求编译器如何做)。
3
为了解决上述问题,C++ 提供了 inline 函数,有效地确保参数中的表达式只被展开和计算一次:
inline int min(int a, int b) { return (a < b ? a : b); } inline int max(int a, int b) { return (a > b ? a : b); } 通过这样的方式,代码得到了简化(不需要在三目运算中,为每个变量都加一个额外的小括号),而且还能确保类型安全。而其中定义的 int 类型,还可以通过模板进行泛化,使之适用于任何拥有比较功能的类型(即支持运算符“<”与“>”的类型):
template
4
此外,在类型方面,C++从语法层面,也提供了很多便利和方法,帮助代码的简化。
例如,对于结构体类型定义,在早期的 C 语言中,定义和使用,都需要带上 struct 关键字:
struct foo // 类型定义 { int a, b, c; }; struct foo f; // 使用该结构体类型,定义一个变量 在很多 C 语言 API 的头文件定义中,我们经常可以学习到类似如下的写法:
typedef struct foo // 在定义类型的同时,用 typedef 提供一个别名 { int a, b, c; } foo_t; foo_t f; // 此时,可以用 “foo_t” 代替 “struct foo” 这也是很多系统底层函数中,类型名经常会带有“_t”结尾,其实大多都是源自一个 struct 类型的别名定义,目的是让使用的时候,不用每次都繁琐地加上“struct”。
这个问题在 C++ 中,从语法层面得到了简化。C++ 的 struct 几乎与 class 是完全等同的,唯一差别仅在于缺省的成员访问权限,struct 是 public ,而 class 是 private。所以,一旦通过 struct (或 class)定义了某个类型,则在使用该类型的时候,可以直接省略“struct”关键字,只写出类型名即可。
struct foo // 类型定义 { int a, b, c; }; struct foo f; // C的写法 foo f; // C++的写法 5
回到 decltype 关键词。以一个简单例子来说明它的用法:
int a = 1; struct A { double x; }; const A* p;
decltype(a) b; // 获取变量 a 的类型,用来定义 b // 相当于:int b; decltype(p->x) y; // 获取变量 p->x 的类型,用来定义 y // 相当于 double y; 单从上面的例子看,decltype 这个关键词的确似乎没啥实际用途,像是个类型别名,但实际上却反而让代码更加晦涩了。每次看一个变量类型,都得跳转看看相应其他借用来进行定义的变量的类型,才能知晓。
其实,decltype 的通常用法,是帮助对函数返回值进行推导的:
template <typename Container, typename Index> auto GetElement(Container& c, Index i) -> decltype(c[i]) { doSomeChecking(); return c[i]; } 上面这个例子,函数的返回值,需要根据容器的元素类型来决定,通过 auto(返回值型别尾序语法)与 decltype 配合,使得编译器可以通过 operator[] 的实际情况,来定义 GetElement 函数的返回值类型,从而写出尽可能普适的代码。
6
不过,对于 decltype 的深入挖掘(这里暂不详细展开,可以通过阅读“条款3”的详细介绍,来初步窥探其难以驾驭的一面),会发现它甚至在语法上,出现了某些“不太符合直觉”的情况。举书中一个例子:
decltype(auto) f1() { int x = 0; … return x; // 返回类型是 int }
decltype(auto) f2() { int x = 0; … return (x); // 返回类型是 int& } 这样的细节,以及它背后的原理,编译器对代码是如何理解的,对这些知识的掌握程度,体现了不同 C++ 程序员之间的水平高低。
注:本文首发表于“不靠谱颜论”公众号,并同步至本站。