【为宏正名】for的妙用你想不到
【说在前面的话】
【被低估的价值】
for ( init_clause ; cond_expression ; iteration_expression )
loop_statement
for 循环中的 cond_expression 和 interation_expression 都必须是表达式,而不能是直接的语句。
for 循环中第一个部分 init_clause 一开始是用来放置给变量赋值的表达式;但从ANSI-C99开始,init_clause 可以被用来建立局部变量;而局部变量的生命周期覆盖且仅覆盖整个for循环——这一点非常有利用价值,也是大家容易忽略的地方。
int i = 0;
...
for (i = 0; i < 100; i++) {
...
}
int i = 0, j,k;
...
for (i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, short j = 100; i < 100; i++) {
...
}
for (int i = 0, *p = NULL; i < 100; i++) {
...
}
另外一个值得注意的是 for 的执行顺序,它可以用下面的流程图来表示:
图中灰色的部分为原本实际的执行流程,而纯黑色的线条以及最下方的虚线箭头则为等效的运行流程。与do {} while(0) 相比,在我们眼中 for 循环的几个关键部分就有了新的意义:
在执行用户代码之前(灰色部分),有能力进行一定的“准备工作”(Before部分);
在执行用户代码之后,有能力执行一定的“收尾工作”(After部分)
在init_clause阶段有能力定义一个“仅仅只覆盖” for 循环的,并且只对 User Code可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的地方的。
【构造using结构】
using (StreamReader tReader = File.OpenText(m_InputTextFilePath))
{
while (!tReader.EndOfStream)
{
...
}
}
以上述代码为例进行讲解:
在 using 圆括号内定义的变量,其生命周期仅覆盖 using 紧随其后的花括号内部;
当用于代码离开 using 结构的时候,using 会自动执行一个“扫尾工作”,而这个扫尾工作是对应的类事先定义好的。在上述例子中,所谓的扫尾工作就是关闭 与 类StreamReader的实例tReader 所关联的文件——简单说就是using会自动把文件关闭,而不必用户亲自动手。
for (int i = 1; i > 0; i++) {
...
}
以此为起点,对比我们的“蓝图”,发现至少有以下几个问题:
如何实现 before和after的部分?
现在用的变量 i 固定是 int 类型的,如何允许用户在 init_clause 定义自己的局部变量,并允许使用自己的类型?
问题一:如何实现 before 和 after 部分
//! 假设用户要插入的内容我们都放在叫做 before 和after的函数里
extern void before(void);
extern void after(void);
for (int i = 1; //!< init_clause
i--?(before(),1):0; //!< cond_expression
after()) //!< iteration_expression
{
...
}
(i--) ? 1 : 0
(i--) ?
(before(), 1) //!< 使用逗哈表达式进行扩展
: 0
由于逗号表达式只管 最右边的结果,忽略所有左边的返回值,因此,哪怕before()函数没有实际返回值对C编译器来说都是无所谓的。同理,由于我们在cond_expression部分已经完成了所有功能,因此 iteration_expression 就任由我们宰割了——编译器原本就对此处表达式所产生的数值并不感兴——我们直接放下 after() 函数即可。
#define using(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *_ptr = NULL; \
_ptr++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
using(int a = 0,printf("========= On Enter =======\r\n"),
printf("========= On Leave =======\r\n"))
{
printf("\t In Body a=%d \r\n", ++a);
}
这是对应的执行效果:
我们不妨将上述的宏进行展开,一个可能的结果是:
for (int a = 0, *_ptr = NULL;
_ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0;
printf("========= On Leave =======\r\n") )
{
printf("\t In Body a=%d \r\n", ++a);
}
从 init_clause 的展开结果来看,完全符合要求:
int a = 0, *_ptr = NULL;
#define using(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
这里,实际上是使用了前面文章中介绍的宏 CONNECT3() 将 “__using_”,__LINE__所表示的当前行号,以及 "_ptr" 粘连在一起,形成一个唯一的局部变量名:
CONNECT3(__using_, __LINE__,_ptr)
如果你对 CONNECT() 宏的来龙去脉感兴趣,可以单击这里。
#define __using1(__declare) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL; \
)
#define __using2(__declare, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL; \
__on_leave_expr \
)
#define __using3(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
#define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr) \
for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
借助宏的重载技术,我们可以根据用户输入的参数数量自动选择正确的版本:
#define using(...) \
CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
至此,我们完成了对 for 的改造,并提出了__using1, __using2, __using3 和 __using4 四个版本变体。那么问题来了,他们分别有什么用处呢?
【提供不阻碍调试的代码封装】
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t CONNECT2(temp, __LINE__) = __disable_irq(); \
__VA_ARGS__ \
__set_PRIMASK((CONNECT2(temp, __LINE__))); \
}
因此可以很容易的通过如下的代码来保护关键的寄存器操作:
/**
\fn void wr_dat (uint16_t dat)
\brief Write data to the LCD controller
\param[in] dat Data to write
*/
static __inline void wr_dat (uint_fast16_t dat)
{
SAFE_ATOM_CODE (
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */
GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */
LCD_CS(1);
)
}
唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:
#define SAFE_ATOM_CODE() \
__using2( uint32_t CONNECT2(temp,__LINE__) = __disable_irq(), \
__set_PRIMASK(CONNECT2(temp,__LINE__)))
修改上述的代码为:
static __inline void wr_dat (uint_fast16_t dat)
{
SAFE_ATOM_CODE() {
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */
GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */
LCD_CS(1);
}
}
由于using的本质是 for 循环,因为我们可以通过花括号的形式来包裹用户代码,因此,可以很方便的在用户代码中添加断点,单步执行。至于原子保护的功能,我们不妨将上述代码进行宏展开:
static __inline void wr_dat (uint_fast16_t dat)
{
for (uint32_t temp154 = __disable_irq(), *__using_154_ptr = NULL;
__using_154_ptr++ == NULL ? ((temp154 = temp154),1) : 0;
__set_PRIMASK(temp154) )
{
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8);
GLCD_PORT->DAT = (dat & 0xFF);
LCD_CS(1);
}
}
通过观察,容易发现,这里巧妙使用 init_clause 给 temp154 变量进行赋值——在关闭中断的同时保存了此前的状态;并在原本 after 的位置放置了 恢复中断的语句 __set_PRIMASK(temp154)。
在OOPC中自动创建类,并使用 before 部分来执行构造函数;在 after 部分完成 类的析构。
在外设操作中,在 init_clause 部分定义指向外设的指针;在 before部分 Enable或者Open外设;在after部分Disable或者Close外设。
在RTOS中,在 before 部分尝试进入临界区;在 after 部分释放临界区
在文件操作中,在 init_clause 部分尝试打开文件,并获得句柄;在 after 部分自动 close 文件句柄。
在有MPU进行内存保护的场合,在 before 部分,重新配置MPU获取目标地址的访问权限;在 after部分再次配置MPU,关闭对目标地址范围的访问权限。
……
【构造with块】
你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机
WITH 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机
# 这里可以直接访问手机的各项属性,用 “.” 开头就行
. 手机壳颜色 = xxxxx
. 贴膜 = 玻璃膜
END WITH
不光是Visual Basic,我们使用C语言进行大规模的应用开发时,或多或少也会遇到同样的情况,比如,配置 STM32 外设时,填写外设配置结构体的时候,每一行都要重新写一遍结构体变量的名字,也是在是很繁琐:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
s_UARTHandle.Instance = USART2;
s_UARTHandle.Init.BaudRate = 115200;
s_UARTHandle.Init.WordLength = UART_WORDLENGTH_8B;
s_UARTHandle.Init.StopBits = UART_STOPBITS_1;
s_UARTHandle.Init.Parity = UART_PARITY_NONE;
s_UARTHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
s_UARTHandle.Init.Mode = UART_MODE_TX_RX;
入股有了with块的帮助,上述代码可能就会变得更加清爽,比如:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
with(s_UARTHandle) {
.Instance = USART2;
.Init.BaudRate = 115200;
.Init.WordLength = UART_WORDLENGTH_8B;
.Init.StopBits = UART_STOPBITS_1;
.Init.Parity = UART_PARITY_NONE;
.Init.HwFlowCtl = UART_HWCONTROL_NONE;
.Init.Mode = UART_MODE_TX_RX;
}
遗憾的是,如果要完全实现上述的结构,在C语言中是不可能的,但借助我们的 using() 结构,我们可以做到一定程度的模拟:
#define with(__type, __addr) using(__type *_p=(__addr))
#define _ (*_p)
在这里,我们要至少提供目标对象的类型,以及目标对象的地址:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
with(UART_HandleTypeDef &s_UARTHandle) {
_.Instance = USART2;
_.Init.BaudRate = 115200;
_.Init.WordLength = UART_WORDLENGTH_8B;
_.Init.StopBits = UART_STOPBITS_1;
_.Init.Parity = UART_PARITY_NONE;
_.Init.HwFlowCtl = UART_HWCONTROL_NONE;
_.Init.Mode = UART_MODE_TX_RX;
}
注意到,这里“_”实际上被用来替代 s_UARTHandle——虽然感觉有点不够完美,但考虑到脚本语言 perl 有长期使用 "_" 表示本地对象的传统,这样一看,似乎"_" 就是一个对 "perl" 的完美致敬了。
【回归本职 foreach】
typedef struct example_lv0_t {
uint32_t wA;
uint16_t hwB;
uint8_t chC;
uint8_t chID;
} example_lv0_t;
example_lv0_t s_tItem[8] = {
{.chID = 0},
{.chID = 1},
{.chID = 2},
{.chID = 3},
{.chID = 4},
{.chID = 5},
{.chID = 6},
{.chID = 7},
};
我们希望实现一个函数,能通过 foreach 自动的访问数组 s_tItem 的所有成员,比如:
foreach(example_lv0_t, s_tItem) {
printf("Processing item with ID = %d\r\n", _.chID);
}
跟With块一样,这里我们仍然“致敬” perl——使用 "_" 表示当前循环下的元素。在这个例子中,为了使用 foreach,我们需要提供至少两个信息:目标数组元素的类型(example_lv0_t)和目标数组(s_tItem)。
#define dimof(__array) (sizeof(__array)/sizeof(__array[0]))
#define foreach(__type, __array) \
__using1(__type *_p = __array) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, CONNECT2(count,__LINE__)-- \
)
上述的宏并不复杂,大家完全可以自己看懂,唯一需要强调的是,using() 的本质是一个for,因此__using1() 下方的for 实际上是位于由 __using1() 所提供的循环体内的,也就是说,这里的局部变量_p其作用域也覆盖 下面的for 循环,这就是为什么我们可以借助:
#define _ (*_p)
的巧妙代换,通过 “_” 来完成对指针“_p”的使用。为了方便大家理解,我们不妨将前面的例子代码进行宏展开:
for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL;
__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
)
for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
count177 > 0;
_p = _p+1, count177-- )
{
printf("Processing item with ID = %d\r\n", (*_p).chID);
}
其执行结果为:
#define foreach2(__type, __array) \
using(__type *_p = __array) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, CONNECT2(count,__LINE__)-- \
)
#define foreach3(__type, __array, __item) \
using(__type *_p = __array, *__item = _p, _p = _p, ) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, __item = _p, CONNECT2(count,__LINE__)-- \
)
这里的 foreach3 提供了3个参数,其中最后一个参数就是用来由用户“额外”指定新的指针的;与之相对,老版本的foreach我们称之为 foreach2,因为它只需要两个参数,只能使用"_"作为对象的指代。进一步的,我们可以使用宏的重载来简化用户的使用:
#define foreach(...) \
CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
经过这样的改造,我们可以用下面的方法来为我们的循环指定一个叫做"ptItem"的指针:
foreach(example_lv0_t, s_tItem, ptItem) {
printf("Processing item with ID = %d\r\n", ptItem->chID);
}
展开后的形式如下:
for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL;
__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
)
for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
count177 > 0;
_p = _p+1, ptItem = _p, count177-- )
{
printf("Processing item with ID = %d\r\n", ptItem->chID);
}
代码已经做了适当的展开和缩进,这里就不作进一步的分析了。
【后记】
宏不是奇技淫巧
宏可以封装出其它高级语言所提供的“基础设施”
设计良好的宏可以提升代码的可读性,而不是破坏它
设计良好的宏并不会影响调试
宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using() 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险
如果你觉得我的文章对你有所启发或是帮助,
还请“点赞、收藏、转发” 三连!