- 碎碎念
- 原始代码
- 表格驱动写法
- enum 值作为表格索引
- 静态检查
- 辅助宏
static_check_table
- 额外细节讨论
碎碎念
我们说程序写得好,其实包含两层含义,不同语境下有所侧重。
一层含义是程序实现了很厉害的功能。比如某个酷炫的特效,或者某个很高效的算法。另一层含义是,程序表达得很好,清晰易懂,也容易修改。以写文章来类比,一层是文章内容,另一层是表达文笔。
本文说代码写得好,更多是指表达得好。假如代码表达不好,就算有很厉害的算法,写着写着就会乱掉。代码写得乱,就会出错。而这些错误本来是可避免的。
说到写好代码,很多文章都会讨论架构、设计模式、高内聚低耦合、命名规范之类。这些知识自然很重要,但总感觉有点虚有点宏大,有时太宏大了就不知道如何落实,要多写代码才能慢慢理解。
也有些代码技巧,从细处着手,强调写好每一个函数,知道就可用到。让我挑选随学随用的两个小技巧,我会选择:
- ScopeGuard.
- 表格驱动。
关于 ScopeGuard 之前写过文章了,见ScopeGuard 介绍和实现。而表格驱动在《代码大全》第 18 章已有讨论。
此文补充一个表格驱动的 C++ 例子。
原始代码
下面这段代码是我从真实工程中摘抄下来的,是处理图像的一小段代码,只是简化并改改名字。
#include <stdio.h>
#include <assert.h>
typedef enum {
Orientation_Up = 0,
Orientation_Down,
Orientation_Left,
Orientation_Right,
Orientation_UpMirrored,
Orientation_DownMirrored,
Orientation_LeftMirrored,
Orientation_RightMirrored,
} OrientationType;
static void doSomething(OrientationType oriType) {
bool needMirror = false;
int rotation = 0;
switch (oriType) {
case Orientation_Up:
rotation = 0;
needMirror = false;
break;
case Orientation_Down:
rotation = 180;
needMirror = false;
break;
case Orientation_Left:
rotation = 270;
needMirror = false;
break;
case Orientation_Right:
rotation = 90;
needMirror = false;
break;
case Orientation_UpMirrored:
rotation = 0;
needMirror = true;
break;
case Orientation_DownMirrored:
rotation = 180;
needMirror = true;
break;
case Orientation_LeftMirrored:
rotation = 270;
needMirror = true;
break;
case Orientation_RightMirrored:
rotation = 90;
needMirror = true;
break;
}
printf("rotation = %dn", (int)rotation);
printf("needMirror = %dn", (int)needMirror);
}
int main(int argc, const char *argv[]) {
doSomething(Orientation_UpMirrored);
return 0;
}
doSomething
函数中的那个 switch, case 是很典型的写法。其实也没有什么大问题,只是还不够清晰,另外要加个新类型,就需要添加一个 case。
表格驱动写法
这种典型的 switch, 每个 case 有很相似代码,可以写成表格驱动风格。
static void doSomething(OrientationType oriType) {
const struct Info {
OrientationType type;
int rotation;
bool needMirror;
} infos[] = {
{Orientation_Up, 0, false},
{Orientation_Down, 180, false},
{Orientation_Left, 270, false},
{Orientation_Right, 90, false},
{Orientation_UpMirrored, 0, true},
{Orientation_DownMirrored, 180, true},
{Orientation_LeftMirrored, 270, true},
{Orientation_RightMirrored, 90, true},
};
bool needMirror = false;
int rotation = 0;
for (size_t idx = 0; idx < sizeof(infos) / sizeof(infos[0]); idx++) {
if (infos[idx].type == oriType) {
needMirror = infos[idx].needMirror;
rotation = infos[idx].rotation;
break;
}
}
printf("rotation = %dn", (int)rotation);
printf("needMirror = %dn", (int)needMirror);
}
原始代码中,以 oriType 作为 switch 的选项。case 中有 rotation、needMirror 两项,于是表格就有对应的三项。这里的表格是个数组,有时表格也可以是个字典。根据 type 字段,在表格中线性查找,找到表格对应的项。
不用担心表格的初始化耗时,在编译优化时,表格就会被初始化,并不是每次调用 doSomething
才初始化。新代码短了很多,也更加容易修改。当新加一个类型,只需要在表格中插入一个记录。
到这里,已经了解了表格驱动的风格是怎么样的。通常代码修改到这里也就够了。
enum 值作为表格索引
上面代码绝大多数情况下都没有问题。但假如这段代码出现在较高性能的场合,那个 for 循环线性遍历,让人有点不安。注意到 enum 的值为
typedef enum {
Orientation_Up = 0,
Orientation_Down,
Orientation_Left,
Orientation_Right,
Orientation_UpMirrored,
Orientation_DownMirrored,
Orientation_LeftMirrored,
Orientation_RightMirrored,
} OrientationType;
enum 从 0 开始,这种情况也很典型。而数组索引也从 0 开始,因而只要表格定义的顺序跟 enum 对应,enum 的值可以直接作为表格的索引。
static void doSomething(OrientationType oriType) {
const struct Info {
OrientationType type;
int rotation;
bool needMirror;
} infos[] = {
{Orientation_Up, 0, false},
{Orientation_Down, 180, false},
{Orientation_Left, 270, false},
{Orientation_Right, 90, false},
{Orientation_UpMirrored, 0, true},
{Orientation_DownMirrored, 180, true},
{Orientation_LeftMirrored, 270, true},
{Orientation_RightMirrored, 90, true},
};
bool needMirror = false;
int rotation = 0;
const size_t tableIdx = (size_t)oriType;
if (tableIdx < sizeof(infos) / sizeof(infos[0])) {
assert(infos[tableIdx].type == oriType);
needMirror = infos[tableIdx].needMirror;
rotation = infos[tableIdx].rotation;
}
printf("rotation = %dn", (int)rotation);
printf("needMirror = %dn", (int)needMirror);
}
上述代码用 oriType 作为索引,就可省略掉那个 for 循环。那个 if 用于判断是否数组越界,有时可以省略。
注意上面代码加了个 assert。
assert(infos[tableIdx].type == oriType);
假如手误,写错了表格顺序。或者 enum 的值被改变,程序运行到这里就会触发 assert,于是有一次动态检查。
静态检查
到了这里,代码算漂亮了,速度也快,只是还是有隐患。上面代码有个隐含条件,需要表格项的索引跟 enum 对应。但假如有一天修改了 enum 的值,或者中途在表格中插入一项,代码就错了。
这里的隐患条件可以添加一个注释,但还不够。那个 assert 只会在运行时检查,更好的方式是在编译就做检查,假如写错就编译不过,这样可以引起程序员的注意。可以添加注释并加 static_assert
检查,就会变成
static void doSomething(OrientationType oriType) {
// 以 oriType 为索引,表格项定义必须跟 enum 值对应
constexpr struct Info {
OrientationType type;
int rotation;
bool needMirror;
} infos[] = {
{Orientation_Up, 0, false},
{Orientation_Down, 180, false},
{Orientation_Left, 270, false},
{Orientation_Right, 90, false},
{Orientation_UpMirrored, 0, true},
{Orientation_DownMirrored, 180, true},
{Orientation_LeftMirrored, 270, true},
{Orientation_RightMirrored, 90, true},
};
static_assert(infos[0].type == 0, "wrong type");
static_assert(infos[1].type == 1, "wrong type");
static_assert(infos[2].type == 2, "wrong type");
static_assert(infos[3].type == 3, "wrong type");
static_assert(infos[4].type == 4, "wrong type");
static_assert(infos[5].type == 5, "wrong type");
static_assert(infos[6].type == 6, "wrong type");
static_assert(infos[7].type == 7, "wrong type");
bool needMirror = false;
int rotation = 0;
const size_t tableIdx = (size_t)oriType;
if (tableIdx < sizeof(infos) / sizeof(infos[0])) {
assert(infos[tableIdx].type == oriType);
needMirror = infos[tableIdx].needMirror;
rotation = infos[tableIdx].rotation;
}
printf("rotation = %dn", (int)rotation);
printf("needMirror = %dn", (int)needMirror);
}
修改后,假如表格中改变顺序或者 enum 值被修改,就会触发 static_assert
,导致编译不过。constexpr 是 C++ 11 的关键字,C 似乎没有对应的东西。上面那段代码,在 C 中编译不过。
辅助宏 static_check_table
到了这里,代码基本可以了。但还是有问题,就是那个 static_assert
的检查有点啰嗦,有点重复。下一个表格又要写同样的代码。这时可以写一些辅助静态检查的宏, 比如:
#define static_check_concat_(A, B) A##B
#define static_check_concat(A, B) static_check_concat_(A, B)
#define static_check_table_0(tableName, typeName)
#define static_check_table_1(tableName, typeName) static_assert(infos[0].typeName == 0, "wrong type")
#define static_check_table_2(tableName, typeName)
static_check_table_1(tableName, typeName);
static_assert(infos[1].typeName == 1, "wrong type")
#define static_check_table_3(tableName, typeName)
static_check_table_2(tableName, typeName);
static_assert(infos[2].typeName == 2, "wrong type")
#define static_check_table_4(tableName, typeName)
static_check_table_3(tableName, typeName);
static_assert(infos[3].typeName == 3, "wrong type")
#define static_check_table_5(tableName, typeName)
static_check_table_4(tableName, typeName);
static_assert(infos[4].typeName == 4, "wrong type")
#define static_check_table_6(tableName, typeName)
static_check_table_5(tableName, typeName);
static_assert(infos[5].typeName == 5, "wrong type")
#define static_check_table_7(tableName, typeName)
static_check_table_6(tableName, typeName);
static_assert(infos[6].typeName == 6, "wrong type")
#define static_check_table_8(tableName, typeName)
static_check_table_7(tableName, typeName);
static_assert(infos[7].typeName == 7, "wrong type")
#define static_check_table(tableName, N, typeName)
static_assert(sizeof(tableName) / sizeof(tableName[0]) == N, "wrong table size");
static_check_concat(static_check_table_, N)(tableName, typeName)
有了 static_check_table
宏,代码可以写成
static void doSomething(OrientationType oriType) {
// 以 oriType 为索引,表格项定义必须跟 enum 值对应
constexpr struct Info {
OrientationType type;
int rotation;
bool needMirror;
} infos[] = {
{Orientation_Up, 0, false},
{Orientation_Down, 180, false},
{Orientation_Left, 270, false},
{Orientation_Right, 90, false},
{Orientation_UpMirrored, 0, true},
{Orientation_DownMirrored, 180, true},
{Orientation_LeftMirrored, 270, true},
{Orientation_RightMirrored, 90, true},
};
static_check_table(infos, 8, type);
bool needMirror = false;
int rotation = 0;
const size_t tableIdx = (size_t)oriType;
if (tableIdx < sizeof(infos) / sizeof(infos[0])) {
assert(infos[tableIdx].type == oriType);
needMirror = infos[tableIdx].needMirror;
rotation = infos[tableIdx].rotation;
}
printf("rotation = %dn", (int)rotation);
printf("needMirror = %dn", (int)needMirror);
}
static_check_table
中,第一个参数是表格名字,第二个参数是表格大小,第三个参数是需要检查的关键字。
static_check_table(infos, 8, type);
展开为
static_assert(sizeof(infos) / sizeof(infos[0]) == 8, "wrong table size");
static_assert(infos[0].type == 0, "wrong type");
static_assert(infos[1].type == 1, "wrong type");
static_assert(infos[2].type == 2, "wrong type");
static_assert(infos[3].type == 3, "wrong type");
static_assert(infos[4].type == 4, "wrong type");
static_assert(infos[5].type == 5, "wrong type");
static_assert(infos[6].type == 6, "wrong type");
static_assert(infos[7].type == 7, "wrong type");
额外细节讨论
有些人可能认为,那个 static_check_table
反而使得代码更多,更复杂了。但是 static_check_table
是通用的,并不依赖于具体场合,可以重复使用。比如另一段类似代码
typedef enum {
SpeedMode_Default = 0,
SpeedMode_SlowX2 = 1,
SpeedMode_SlowX3 = 2,
SpeedMode_SlowX4 = 3,
SpeedMode_FastX2 = 4,
SpeedMode_FastX3 = 5,
SpeedMode_FastX4 = 6,
} SpeedMode;
static float getSpeedRate(SpeedMode mode) {
switch (mode) {
case SpeedMode_FastX2:
return 2.0f;
case SpeedMode_FastX3:
return 3.0f;
case SpeedMode_FastX4:
return 4.0f;
case SpeedMode_SlowX2:
return 1 / 2.0f;
case SpeedMode_SlowX3:
return 1 / 3.0f;
case SpeedMode_SlowX4:
return 1 / 4.0f;
default:
return 1.0f;
}
}
就可以相应修改为
static float getSpeedRate(SpeedMode mode) {
constexpr struct Info {
SpeedMode mode;
float value;
} infos[] = {
{SpeedMode_Default, 1.0f},
{SpeedMode_SlowX2, 1 / 2.0f},
{SpeedMode_SlowX3, 1 / 3.0f},
{SpeedMode_SlowX4, 1 / 4.0f},
{SpeedMode_FastX2, 2.0f},
{SpeedMode_FastX3, 3.0f},
{SpeedMode_FastX4, 4.0f},
};
static_check_table(infos, 7, mode);
const size_t tableIdx = (size_t)mode;
if (tableIdx < sizeof(infos) / sizeof(infos[0])) {
assert(infos[tableIdx].mode == mode);
return infos[tableIdx].value;
}
return 1.0f;
}
这种 switch、case 的代码是很典型的。
另外一些细节
- C 中没有 constexpr,做不了数组的静态检查。
- 上面默认了 enum 从 0 开始,但很可能 enum 开始值是其他。线性查找下,枚举就算从其他值开始也没有所谓。而假如 enum 作为数组索引,就需要减去相应偏移值。
static_check_table
也需要相应修改,添加一个偏移值的参数。