跳到主要内容
左猫娘右猫娘

【八嘎C++】悬挂引用

· 阅读需 10 分钟
Heng_Xin
ここから先は一方通行だ!

近日一名闲的没事干的小火子, 打算为远古的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));
}
cpp
  • 最初 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!
cpp

但是事实真的是这样吗?

Note

根据 C++17 §12.2/5

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));
}
cpp

返回的是 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;
}();
cpp

核心就是这样; 剩下只能多避免了; 没事不要写 A&& 作为返回值;

常见的使用场景应该是链式调用, 而 不是 返回临时对象:

struct A {
[[nodiscard]] A&& set() && { return std::move(*this); } // 正确的使用场景
};
cpp

同理, 如果使用 decltype(auto) 也要特别小心, 小心它被推导为 T&&!

请作者喝奶茶:
Alipay IconQR Code
Alipay IconQR Code
本文遵循 CC CC 4.0 BY-SA 版权协议, 转载请标明出处
Loading Comments...