前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >defer in C++/Objc

defer in C++/Objc

原创
作者头像
forrestlin
修改2020-06-11 10:35:44
9780
修改2020-06-11 10:35:44
举报
文章被收录于专栏:蜉蝣禅修之道蜉蝣禅修之道

导语

写过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个的函数指针作为其参数,在声明的作用域结束之前执行指定的函数。文字说明可能不够清楚,参考下列代码:

// 指定一个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指令,那么就可以在作用域结束前执行指定的代码,正如以下代码所示:

// 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宏,我们可以定义以下的宏:

#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宏,上面那一长串的声明语句就可以简化为:

{
    @onExit {
        NSLog(@"I'm dying...");
    };
} // 这里输出"I'm dying..."

__LINE__

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

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

完整的定义如下:

#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的功能,如下列代码所示:

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中修改函数的返回值,那么很抱歉,这是没有意义的事情,就好比下列代码:

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 关键字

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • defer的作用
  • defer的范围
  • defer with cleanup
    • __attribute__
      • cleanup
        • __LINE__
        • defer with dealloc
        • defer VS return
        • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档