【八嘎C++】悬挂引用
近日一名闲的没事干的小火子, 打算为远古的iocp对接协程并且支持超时; 以统一io_uring侧API;
故此需要伺候 MSVC、Win32 API
并且为了实现无单例架构和追求简易API原则.
使用了 建造者模式来封装 API...
一不小心 (说是不是故意的?) 就写了 悬挂引用了... 然后出现八嘎了
一、悬挂左值引用
下面代码, 实际上至少存在两处错误. 这里仅讨论其中的一个, 也就是 悬挂左值引用
, 你可以看出来到底是哪里开始悬挂的吗?
太烧脑了? 那我写在一起:
然后最终是调用:
TimerAwaiter&& TimerAwaiterBuilder::sleepFor(
std::chrono::system_clock::duration duration
) && {
return std::move(TimerAwaiter{_timerLoop}.setExpireTime(
std::chrono::system_clock::now() + duration));
}
-
最初
TimerAwaiterBuilder
的_timerLoop
是合法的 -
但是返回后:
TimerAwaiterBuilder
析构, 然后_timerLoop
就是悬挂的了 -
即便此时
TimerAwaiter{_timerLoop}
还在生命周期, 但是 传递的_timerLoop
已经悬挂了
等价于:
struct Data {
int& data;
};
int a = 0;
Data* c = nullptr;
{
int& b = a;
c = new Data{b};
}
// b 析构, b 为悬挂引用
// 那么, c.data = b, 实际上也是悬挂引用!
// 所以, 不应该使用 并查集 思想看 引用, 引用本身不会传递!
// 引用的本质只是别名! 所以 c.data 实际上是 b 的别名
// 但是不等价于 a 的别名, 只是修改 b 的时候, 正好等价于修改 a!
但是事实真的是这样吗?
Note
All temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created, and if multiple temporary objects were created, they are destroyed in the order opposite to the order of creation. This is true even if that evaluation ends in throwing an exception.
所有临时对象都作为评估完整表达式的最后一步被销毁, 该表达式(词法上)包含它们的创建点, 如果创建了多个临时对象, 则它们将按与创建顺序相反的顺序销毁。即使该评估以引发异常结束, 也是如此。
因此, co_await makeTimer().sleepFor(3s);
的 TimerAwaiterBuilder
还没有析构! (至少在 sleepFor
中是这样)
但是, 八嘎没有结束, 核心八嘎是下面...
二、悬挂右值引用
为了保险起见 (实际上是尝试了很多地方后), 现在代码变成下面这样了:
在MSVC上, 会被检测出来; 如果是在力扣提交 (因为力扣默认是clang18 (C++23) + 开启
Address Sanitizer
检测的)运行, 都抛异常了;
但是, Release 下, 全都没有问题: https://godbolt.org/z/KqzP1T68E (godbolt好像默认都是Release运行的?)
代码:
我直接给出答案(问题所在)了:
A&& BuildA::build(std::chrono::system_clock::duration t) && {
return A{_md}.setExpireTime(makeTime(t));
}
返回的是 A&&
! 当返回时候, A{_md}.setExpireTime(makeTime(t))
就析构了
即便使用 A =
这种语法来接住, 实际上只是语法通过了; 但是实际上还是指向了 悬挂引用 A&&
不信可以自己调试一下:
auto __test02__ = [] {
HX::print::println("Test02 {");
struct A {
A&& todo() && {
return std::move(*this);
}
static A&& mk() {
return std::move(A{}.todo());
}
~A() noexcept {
HX::print::println("~A ", this);
(void)_p;
}
private:
using MdTree = std::multimap<std::chrono::system_clock::time_point, int>;
std::optional<MdTree::iterator> _p{};
};
struct ABuild {
A&& build() && {
return std::move(A{}.todo());
}
~ABuild() noexcept {
HX::print::println("~ABuild ", this);
}
};
auto makeBuild = []{
return ABuild{};
};
{
auto&& a = makeBuild().build();
(void)a;
HX::print::println("--- mk End a ---\n");
}
HX::print::println("--- --- --- --- ---\n");
{
decltype(auto) b = makeBuild().build();
(void)b;
HX::print::println("--- mk End b ---\n");
}
HX::print::println("--- --- --- --- ---\n");
{
auto c = makeBuild().build();
(void)c;
HX::print::println("--- mk End c ---\n");
}
HX::print::println("--- --- --- --- ---\n");
{
auto func = [](A a) {
(void)a;
HX::print::println("func\n");
return;
};
// 下面这行打断点
func(makeBuild().build()); // ub
// 调试的时候就可以发现:
// return std::move(A{}.todo());
// 发生了析构, 才进行返回!
// 因此访问的是悬挂引用!
HX::print::println("--- mk End d ---\n");
}
HX::print::println("--- --- --- --- ---\n");
HX::print::println("} // Test02");
return 0;
}();
核心就是这样; 剩下只能多避免了; 没事不要写 A&&
作为返回值;
常见的使用场景应该是链式调用, 而 不是 返回临时对象:
struct A {
[[nodiscard]] A&& set() && { return std::move(*this); } // 正确的使用场景
};
同理, 如果使用 decltype(auto)
也要特别小心, 小心它被推导为 T&&
!