defer in C++/Objc

在OC和C++中玩一下defer

Posted by Forrest Lam on 2019-12-22

写过swift的同学应该都知道defer这个关键字,可以让我们在函数return之前执行指定的代码,这对于有多个提前return而忘记释放资源的函数来说,简直不要太方便了,然而对于swift的前辈Objective-C或C++来说,苹果并没有帮我们定义,因此本文总结一下如何在C++和Objective-C中实现defer。

defer的作用

正如导语所言,defer关键字可以帮我们在函数返回之前执行指定的代码,其中最常见的作用就是帮我们清理资源,防止某个地方提前return而导致内存泄露。

defer的范围

虽然我们总是拿defer来帮函数做资源回收工作,但其实defer的作用范围是最近的作用域,假如我们将defer放入if作用域中时,defer就会在if作用域结束前执行,而非函数return前,这需要在使用defer多加小心,不然资源提前释放会导致野指针。

此外当一个作用域定义了多个defer,那么退出作用域前其执行顺序就像栈一样,先进后出。

defer with cleanup

_attribute_

想要在Objective-C中完美实现defer,那么我们需要了解一下GNU C中的编译指令__attribute__((attribute-list)),该编译指令的括号里可以填非常多的指令,例如format可以用来帮助printf检查格式化字符串的参数类型对不对,又例如noreturn用来告知编译器该函数并不是所有条件下都有返回值,编译时不需要输出warning,而我们现在需要用的是cleanup指令。

cleanup

cleanup指令可以说是非常符合我们当前的需求,该指令接受一个返回为空,参数个数是1个的函数指针作为其参数,在声明的作用域结束之前执行指定的函数。文字说明可能不够清楚,参考下列代码:

1
2
3
4
5
6
7
8
9
// 指定一个cleanup方法,注意入参是所修饰变量的地址,类型要一样
// 对于指向objc对象的指针(id *),如果不强制声明__strong默认是__autoreleasing,造成类型不匹配
static void stringCleanUp(__strong NSString **string) {
NSLog(@"%@", *string);
}
// 在某个方法中:
{
__strong NSString *string __attribute__((cleanup(stringCleanUp))) = @"sunnyxx";
} // 当运行到这个作用域结束时,自动调用stringCleanUp

借助cleanup这个黑魔法,假如我们定义一个接受一个block指针参数的函数,其函数体就是直接执行该block参数,然后将该函数传给cleanup指令,那么就可以在作用域结束前执行指定的代码,正如以下代码所示:

1
2
3
4
5
6
7
8
9
10
// void(^block)(void)的指针是void(^*block)(void)
static void blockCleanUp(__strong void(^*block)(void)) {
(*block)();
}
{
// 加了个`unused`的attribute用来消除`unused variable`的warning
__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^{
NSLog(@"I'm dying...");
};
} // 这里输出"I'm dying..."

虽然上面的代码已经可以基本实现我们的需求,但是假如每次使用都要敲上面这么长的声明变量语句,怕是很难记住,因此,参考Reactive Cocoa中神奇的@onExit宏,我们可以定义以下的宏:

1
2
3
4
#define ext_keywordify autoreleasepool {}
#define onExit \
ext_keywordify \
__strong ext_cleanupBlock_t ext_exitBlock_ __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^

其中ext_keywordify这个工具宏完全是为了让我们在onExit前添加@,显得更加特别而使用的,也为了更接近Reacive Cocoa而加的。通过onExit宏,上面那一长串的声明语句就可以简化为:

1
2
3
4
5
{
@onExit {
NSLog(@"I'm dying...");
};
} // 这里输出"I'm dying..."

_LINE_

@onExit到这里可以说已经非常接近defer的功能了,但依然还差一点,就是@onExit一个作用域只能声明一次,这是因为onExit宏中我们声明的变量名是ext_exitBlock_,这个固定的名字,所以相同作用域中不能有两个相同的名字的变量,否则编译就会出错。为了解决该问题,我们还需要借用__LINE__宏(__COUNTER__也可以),该宏会在编译后被替换为文件中所在的行号,所以假如我们将ext_exitBlock_这个变量名和行号混在一起,那么就不会有重复的变量名了,因此onExit宏最终的定义如下:

1
2
3
#define onExit \
ext_keywordify \
__strong ext_cleanupBlock_t tt_string_concat(ext_exitBlock_, __LINE__) __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^

完整的定义如下:

1
2
3
4
5
6
7
8
9
#define ext_keywordify autoreleasepool {}
typedef void (^ext_cleanupBlock_t)(void);
void ext_executeCleanupBlock(__strong ext_cleanupBlock_t _Nonnull * _Nonnull block) {
(*block)();
}

#define onExit \
ext_keywordify \
__strong ext_cleanupBlock_t tt_string_concat(ext_exitBlock_, __LINE__) __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^

以上代码都是ObjC的,但__attribute__编译指令是GNU通用的,所以在C++也可以用同样的方法,只是block参数替换为C++11的std::function,然后传入一个lambda函数就可以,这里就不赘述了。

defer with dealloc

defer的第二种实现可以借助局部变量的析构函数,因为局部变量会在调用堆栈返回前释放,这与defer的作用有点相似,故此我们稍加改造也可以实现defer的功能,如下列代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename Function>
struct doDefer {
Function f;
doDefer(Function f): f(f) {}
~doDefer() { f(); }
};

template <typename Function>
triton::doDefer<Function> deferer(Function f) {
return doDefer<Function>(f);
}

#define DEFER_1(x, y) x##y
#define DEFER_2(x, y) DEFER_1(x, y)
#define DEFER_0(x) DEFER_2(x, __LINE__)
#define defer(expr) auto DEFER_0(_defered_option) = deferer([&](){expr;});

上述代码会在作用域结束时执行指定的lambda函数,而且同样的,我们让局部变量的名字后面加上行号,使得可以声明多个defer表达式。

defer VS return

在使用defer过程中,我们需要注意一点,假如我们在defer中修改函数的返回值,那么很抱歉,这是没有意义的事情,就好比下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int test {
int __block result = 1;
@onExit {
result++;
};
return result;
}

void main() {
int res = test();
printf("test res: %d\n", res); //test res: 1
return;
}

test函数中声明了@onExit block,并修改了返回值,但main函数调用完test函数后,res这个返回值依然是1。究其原因,就是因为return语句并不是原子语句,在test函数return时,执行的顺序是确定返回值result = 1 -> 执行@onExit -> 函数返回,因此即使@onExit中修改了返回值,对于最终的函数返回值来说是没有改变的。

参考资料

黑魔法_attribute_\((cleanup))
使用 C/C++ 模拟 defer 关键字