首先,请注意该示例有一个双重释放错误。如果调用了移动构造函数,sp.buffer 不会设置为nullptr,因为它必须是,所以现在存在两个指向缓冲区的指针,以便稍后删除。正确管理指针的更简单的版本是:
struct Simple {
std::unique_ptr<int[]> buffer {new int[1000]};
};
有了这个修复,让我们内联几乎所有内容,看看 foo3 真正做到了哪些荣耀:
using func_t = std::function<int(Sample&&)>&&;
int foo3(func_t func) {
int* buffer1 = new int[1000]; // the unused local
int* buffer2 = new int[1000]; // the call argument
if (!func) {
delete[] buffer2;
delete[] buffer1;
throw bad_function_call;
}
try {
int retval = func(buffer2); // <-- the call
} catch (...) {
delete[] buffer2;
delete[] buffer1;
throw;
}
delete[] buffer2;
delete[] buffer1;
return retval; // <-- the return
}
buffer1 的情况很简单。它是一个未使用的局部变量,唯一的副作用是分配和释放,编译器可以跳过。一个足够聪明的编译器可以完全删除未使用的本地。 clang++ 5.0 似乎可以做到这一点,但 g++ 7.2 没有。
更有趣的是buffer2。 func 采用非常量右值引用。它可以修改参数。例如,它可能会离开它。但它可能不会。临时的可能仍然拥有一个缓冲区,必须在调用后删除,foo3 必须这样做。该调用不是尾调用。
正如观察到的,我们通过简单地泄漏缓冲区来接近尾调用:
struct Simple {
int* buffer = new int[1000];
};
这有点作弊,因为问题的很大一部分是关于面对非平凡析构函数的尾调用优化。但是,让我们来娱乐一下。正如所观察到的,仅此一项并不会导致尾调用。
首先,请注意按引用传递是按指针传递的一种奇特形式。该对象仍然必须存在于某个地方,并且在调用者的堆栈上。需要在调用期间保持调用者的堆栈处于活动状态且非空将排除尾调用优化。
要启用尾调用,我们希望在寄存器中传递func 的参数,因此它不必存在于foo3 的堆栈中。这表明我们应该按值传递:
int foo2(Simple); // etc.
SysV ABI 规定,要在寄存器中传递,它必须是可复制、可移动和可破坏的。作为包装int* 的结构,我们已经涵盖了这一点。有趣的事实:我们不能在这里使用带有无操作删除器的std::unique_ptr,因为它不会被轻易破坏。
即便如此,我们仍然没有看到尾声。我没有看到阻止它的原因,但我不是专家。用函数指针替换std::function 会导致尾调用。 std::function 在调用中有一个额外的参数并且有一个条件抛出。是否有可能使优化变得足够困难?
无论如何,使用函数指针,g++ 7.2 和 clang++ 5.0 进行尾调用:
struct Simple {
int* buffer = new int[1000];
};
int foo2(Simple sp) {
return sp.buffer[std::rand()];
}
using func_t = int (*)(Simple);
int foo3(func_t func) {
return func(Simple());
}
但这是泄漏的。我们能做得更好吗?这种类型有所有权,我们想将它从foo3 传递给func。但是具有非平凡析构函数的类型不能在参数中传递。这意味着像 std::unique_ptr 这样的 RAII 类型不会让我们到达那里。使用 GSL 中的一个概念,我们至少可以表达所有权:
template<class T> using owner = T;
struct Simple {
owner<int*> buffer = new int[1000];
};
那么我们可以希望现在或将来的静态分析工具能够检测到foo2正在接受所有权但永远不会删除buffer。