如果开销是指作用域保护变量占用了多少空间,那么如果功能对象是编译时值,则开销为零是可能的。我编写了小片段来说明这一点:
在线尝试!
#include <iostream>
template <auto F>
class ScopeGuard {
public:
~ScopeGuard() { F(); }
};
void Cleanup() {
std::cout << "Cleanup func..." << std::endl;
}
int main() {
{
char a = 0;
ScopeGuard<&Cleanup> sg;
char b = 0;
std::cout << "Stack difference "
<< int(&a - &b - sizeof(char)) << std::endl;
}
{
auto constexpr f = []{
std::cout << "Cleanup lambda..." << std::endl; };
char a = 0;
ScopeGuard<f> sg;
char b = 0;
std::cout << "Stack difference "
<< int(&a - &b - sizeof(char)) << std::endl;
}
}
输出:
Stack difference 0
Cleanup func...
Stack difference 0
Cleanup lambda...
上面的代码甚至不会在堆栈上创建一个字节,因为任何没有字段的类变量都占用堆栈 0 字节,这是任何编译器都可以完成的明显优化之一。当然,除非您使用指向此类对象的指针,否则编译器必须创建 1 字节内存对象。但是在您的情况下,您不会向范围警卫发送地址。
通过查看代码上方的链接,您可以看到没有一个字节被占用Try it online!
,它显示了 CLang 的汇编器输出。
要完全没有字段范围的保护类应该只使用编译时函数对象,比如没有捕获的 lambda 的全局函数指针。我上面的代码中使用了这两种对象。
在上面的代码中,您甚至可以看到我在作用域保护变量之前和之后输出了 char 变量的堆栈差异,以表明作用域保护实际上占用了 0 个字节。
让我们更进一步,让函数对象有非编译时值的可能性。
为此,我们再次创建没有字段的类,但现在将所有功能对象存储在一个具有线程本地存储的共享向量中。
同样,由于我们在类中没有字段并且不使用任何指向作用域保护对象的指针,因此编译器不会为堆栈上的作用域保护对象创建一个字节。
但相反,单个共享向量在堆中分配。这样,如果您的堆栈内存不足,您可以将堆栈存储换成堆存储。
同样拥有共享向量将允许我们使用尽可能少的内存,因为向量只使用与使用范围保护的嵌套块一样多的内存。如果所有作用域保护按顺序位于不同的块中,则向量内部将只有 1 个元素,因此对于使用的所有作用域保护只使用几个字节的内存。
为什么共享向量的堆内存在内存方面比范围保护的堆栈存储内存更经济。因为在堆栈内存的情况下,如果您有几个连续的警卫块:
void test() {
{
ScopeGuard sg(f0);
}
{
ScopeGuard sg(f1);
}
{
ScopeGuard sg(f2);
}
}
那么所有 3 个守卫在堆栈上占用三倍的内存,因为对于像test()
上面这样的每个函数,编译器为函数变量中使用的所有函数分配堆栈内存,所以对于 3 个守卫,它分配三倍的数量。
如果上面的共享向量test()
函数将仅使用 1 个向量的元素,因此向量的大小最多为 1,因此将仅使用单个内存量来存储功能对象。
因此,如果您在一个函数中有许多非嵌套范围的守卫,那么共享向量将更加经济。
现在,我在下面展示了零字段和零堆栈内存开销的共享向量方法的代码片段。提醒一下,这种方法允许使用非编译时功能对象,这与我的答案之一中的解决方案不同。
在线尝试!
#include <iostream>
#include <vector>
#include <functional>
class ScopeGuard2 {
public:
static auto & Funcs() {
thread_local std::vector<std::function<void()>> funcs_;
return funcs_;
}
ScopeGuard2(std::function<void()> f) {
Funcs().emplace_back(std::move(f));
}
~ScopeGuard2() {
Funcs().at(Funcs().size() - 1)();
Funcs().pop_back();
}
};
void Cleanup() {
std::cout << "Cleanup func..." << std::endl;
}
int main() {
{
ScopeGuard2 sg(&Cleanup);
}
{
auto volatile x = 123;
auto const f = [&]{
std::cout << "Cleanup lambda... x = "
<< x << std::endl;
};
ScopeGuard2 sg(f);
}
}
输出:
Cleanup func...
Cleanup lambda... x = 123