问题很简单:有时我会遇到我修改一些(相当全局的)状态的情况,例如日志级别 - 抢占对全局状态的抱怨:不是我的框架,我无能为力;- ).
为了好看,我应该在完成后恢复旧状态,所以我保存它并在最后恢复它。这是 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++ 中的代码块?
(如果有人觉得有必要用他们最喜欢的不同语言举例,我不介意。)
答案 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";
}
);
}
);
}
hello
hello nested
bye 123
bye 42