问题很简单:有时我会遇到修改某些(相当全局的)状态的情况,例如日志级别——以抢先抱怨全局状态:不是我的框架,我对此无能为力;-)。
为了更好,我应该在完成后恢复旧状态,所以我保存它,并在最后恢复它。这是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