生命周期绑定到代码块的未命名对象?

时间:2021-02-26 16:17:52

标签: c++ lifetime raii

问题很简单:有时我会遇到我修改一些(相当全局的)状态的情况,例如日志级别 - 抢占对全局状态的抱怨:不是我的框架,我无能为力;- ).

为了好看,我应该在完成后恢复旧状态,所以我保存它并在最后恢复它。这是 RAII 的一个明显案例:

// Some header
/// A RAII class which records a state and restores it upon destruction.
struct StateRestorer
{
    State oldState;
    StateRestorer(State oldStateArg) : oldState(oldStateArg) {}
    ~StateRestorer() { setState(oldState); }
};


// Happens a couple times somewhere in my program
{
    StateRestorer oldStateRestorer(getState());
    State newState(/* whatever */);
    setState(newState);
    // Do actually useful things during the new state
    
    // oldStateRestorer goes out of scope and restores the old state.
}

现在,我实际上需要 oldStateRestorer 变量。不要误会我的意思,我确实需要它引用的对象;但我从不以任何方式访问 oldStateRestorer。需要给它起个名字有点麻烦。如果我独自一人,我可能会称它为 s,但我强烈支持良好的命名,以便不熟悉该程序的人(可能是两年后的我)可以很容易地理解它。有时,这些状态变化是嵌套的,因此我必须发明新名称,以免编译器警告我正在隐藏另一个变量(在其他情况下这是一个严重警告,我不喜欢警告开始,现在我都心烦意乱)。正如我所说,有点烦人。

问题归结为:

有没有办法让一个未命名对象的生命周期是 C++ 中的代码块?

(如果有人觉得有必要用他们最喜欢的不同语言举例,我不介意。)

3 个答案:

答案 0 :(得分:3)

<块引用>

有没有办法在 C++ 中拥有一个具有自动存储期的未命名对象?

技术上没有。但是,临时对象非常相似:它们是未命名的并且会自动销毁。此外,可以通过绑定一个引用来延长临时对象的生命周期:

struct silly_example {
    T&& ref;
}

int main()
{
    silly_example has_automatic_storage {
         .ref = T{}; // temporary object
    };
}

在这个例子中,我们有一个具有自动存储功能的命名对象,它引用了一个(n个未命名)临时对象,其生命周期与自动对象的生命周期匹配。

我认为这对您描述的情况没有用。


请注意,这是此类 RAII 类型的典型问题。标准库中的一个示例是 std::lock_guard:

std::mutex some_mutex;

{
    const std::lock_guard<std::mutex>
        must_have_a_name(some_mutex);
    
    // critical section which won't refer to the guard
}

最糟糕的部分不是你必须想出一个名字,而是 const std::lock_guard<std::mutex> (some_mutex); 是一个有效的函数声明,并且会在不创建保护的情况下成功编译。


<块引用>

(如果有人觉得有必要用他们最喜欢的不同语言举例,我不介意。)

Python 特别优雅。因为它没有析构函数,所以它首先不能有这样的 RAII 类型。

some_mutex = Lock()

with some_mutex:
    # critical section here

可以与 with 一起使用的类使用普通函数(具有特定名称)而不是构造函数和析构函数:

class WithExample: 
    def __init__(self, args): 
        pass
      
    def __enter__(self):
        # resource init
        # may return something to be used within the scope
        pass
  
    def __exit__(self): 
        # reource release
        pass

答案 1 :(得分:3)

如果有宏和至少 C++17,你可以使用相当简单的方法:

// Standard issue concatenation macros
#define CONCAT(A, B) CONCAT_(A, B)
#define CONCAT_(A, B) A##B


#define WITH(...) if([[maybe_unused]] auto CONCAT(_dO_nOt_tOuCh, __LINE__)(__VA_ARGS__); true)

带有 init 语句和保证复制省略的 if 是 C++17 的要求。第一个特性很明显,但第二个特性很有用,因为它允许不可复制和不可移动的类型作为 RAII 类型。

有了这个宏,你就可以简单地写

WITH(StateRestorer(getState())) {
  //Your code here.
}

现在,在这一点上,我确定多个 RAII 对象的问题出现了。并且有人可能认为我们要么必须进行大量嵌套,要么如果我们选择编写相当丑陋的代码,则会再次收到警告

WITH(A) WITH(B) WITH(C) {

}

我们可以解决它。或者像我们一样做,但是使用 GNU 特定的 __COUNTER__ 宏而不是 __LINE__。或者通过使用更多的 C++17 来让 WITH 接受一个逗号分隔的 RAII 表达式列表。在合理假设 RAII 类型始终是类的情况下,我们可以执行以下操作

namespace detail {
    template<class... Ts>
    struct glue : Ts... {};

    template<class... Ts>
    glue(Ts...) -> glue<Ts...>;
}
#define WITH(...) if([[maybe_unused]] detail::glue CONCAT(_dO_nOt_tOuCh, __LINE__){__VA_ARGS__}; true)

它使用 CTAD 生成一个类型,该类型继承自以逗号分隔的 RAII 类型列表(并且在很可能的情况下,所有表达式都是纯右值,直接初始化基数)。有了它,我们就可以写

WITH(StateRestorer1(...), StateRestorer2(...)) {
}

当然会以逗号分隔列表的相反顺序调用析构函数。


顺便说一句,这种烦恼是 P0577 (Keep That Temporary!) 背后动机的一部分。论文中有一些有趣的想法,但遗憾的是它没有受到关注。

答案 2 :(得分:1)

我不认为你想要的东西可以直接用 C++ 完成。

但是,如果您的问题是为对象命名,则可以考虑改为调用函数。您不必为函数调用命名:

#include <iostream>

template <typename RAII,typename...Args>
auto with(Args...args){
    return [=](auto f){
        RAII boring_name{args...};
        f();    
    };
}

struct foo { 
    int state;
    foo(int state) : state(state){}
    ~foo(){ std::cout << "bye " << state << "\n";} 
};

int main() {
   with<foo>(42)(
       [&](){
           std::cout << "hello\n";
           with<foo>(123)(
               [&](){
                   std::cout << "hello nested\n";
               }
           );
       }
   );    
}

Output

hello
hello nested
bye 123
bye 42
相关问题