其生存期绑定到代码块的未命名对象



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

为了更好,我应该在完成后恢复旧状态,所以我保存它,并在最后恢复它。这是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变量。不要误解我的意思,我确实需要它所指的对象;但我从未以任何方式访问CCD_ 2。需要给它取个名字有点麻烦。如果我一个人的话,我可能会称之为s,但我强烈支持良好的命名,这样不熟悉该项目的人(可能是两年后的我)就可以很容易地理解它。有时,这些状态变量是嵌套的,所以我必须发明新的名称,以免编译器警告我正在跟踪另一个变量(在其他情况下,这是一个严重的警告,我不喜欢一开始就发出警告,现在我很沮丧)。正如我所说,有点烦人。

问题归结为:

有没有一种方法可以让一个未命名的对象的生存期是C++中的一块代码

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

有没有一种方法可以在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

如果宏和至少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类型的逗号分隔列表继承的类型(在很可能的情况下,所有表达式都是prvalue,直接初始化基)。有了它,我们可以写

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

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


顺便说一句,这种烦恼是P0577(保持暂时性!)背后的部分动机。这篇论文中有一些有趣的想法,但遗憾的是,它没有得到关注。

我不认为您想要的东西可以直接在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 << "hellon";
with<foo>(123)(
[&](){
std::cout << "hello nestedn";
}
);
}
);    
}

输出

hello
hello nested
bye 123
bye 42

最新更新