目前C++(不幸的是)并不支持finally
子句用于try
语句。这导致了如何释放资源的猜测。在互联网上研究了这个问题之后,虽然我找到了一些解决方案,但我对它们的性能不是很清楚(如果性能不那么重要,我会使用Java)。因此,我必须进行基准测试。
可选方案为:
在CodeProject提出的基于函数对象的
finally
类。它很强大,但速度慢。反汇编表明,外部函数局部变量被捕获效率非常低:一个接一个地推到堆栈上,而不是只传递帧指针到内部(lambda)函数。RAII:手动清理对象在堆栈上:缺点是手动输入并为每个使用的地方进行调整。另一个缺点是需要将所有需要释放资源的变量复制到它中。
仅限于MSVC++的
__try
/__finally
语句。缺点是它显然不具备可移植性。
我创建了这个小基准测试来比较这些方法的运行时性能:
#include <chrono>
#include <functional>
#include <cstdio>
class Finally1 {
std::function<void(void)> _functor;
public:
Finally1(const std::function<void(void)> &functor) : _functor(functor) {}
~Finally1() {
_functor();
}
};
void BenchmarkFunctor() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
Finally1 doFinally([&] {
var++;
});
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Functor: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkObject() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
class Cleaner {
volatile int64_t* _pVar;
public:
Cleaner(volatile int64_t& var) : _pVar(&var) { }
~Cleaner() { (*_pVar)++; }
} c(var);
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Object: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkMSVCpp() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
__try {
}
__finally {
var++;
}
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("__finally: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
template <typename Func> class Finally4 {
Func f;
public:
Finally4(Func&& func) : f(std::forward<Func>(func)) {}
~Finally4() { f(); }
};
template <typename F> Finally4<F> MakeFinally4(F&& f) {
return Finally4<F>(std::forward<F>(f));
}
void BenchmarkTemplate() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
auto doFinally = MakeFinally4([&] { var++; });
//Finally4 doFinally{ [&] { var++; } };
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Template: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkEmpty() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
var++;
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Empty: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
int __cdecl main() {
BenchmarkFunctor();
BenchmarkObject();
BenchmarkMSVCpp();
BenchmarkTemplate();
BenchmarkEmpty();
return 0;
}
我的 Ryzen 1800X @3.9Ghz 配置 DDR4 @2.6Ghz CL13 的结果如下:
Functor: 175148825.946 Ops/sec, var=234567890
Object: 553446751.181 Ops/sec, var=234567890
__finally: 553832236.221 Ops/sec, var=234567890
Template: 554964345.876 Ops/sec, var=234567890
Empty: 554468478.903 Ops/sec, var=234567890
显然,除了函数对象基类(functor-base #1)以外的所有选项都像空循环一样快。
那么,有没有一种快速而强大的C ++替代方法来执行finally,在保证便携性和最小从外部函数栈复制的同时呢?
更新:我对@Jarod42的解决方案进行了基准测试,因此在问题中更新了代码和输出。尽管如@Sopel所提到的,如果未执行复制省略可能会出现问题。
更新2:为了澄清我的问题是要在C++中以方便快捷的方式执行一块代码,即使抛出异常也要执行。出于问题中提到的原因,某些方法是慢或不方便的。
finally
子句的一个原因 可能 是在 C++ 中抛出异常很昂贵,因此应该仅在真正异常的情况下使用。当然,这导致try-catch
块不常见,大多用于进行一些错误报告,然后重新抛出异常以终止应用程序。这意味着finally
子句实际上没有用处。这与其他语言不同,其他语言中异常是正常的错误处理函数。 - Some programmer dudefinally
都真正处理资源,有些可能会恢复状态,为每种情况创建RAII类只会重复一种可以通过finally
因式分解的模式。 - Jarod42