在学习 hologres 代码的时候, 忽然发现一处直觉感觉不对的地方, 简化一下对应着如下代码块:
#include <memory>
#include <stdio.h>
using namespace std;
struct C2 {
template <typename F>
void finally(F f) {
f();
}
};
struct C1 {
C2 Close() {
printf("this: %p\n", this);
return C2{};
}
};
template <typename T>
struct SharedPtr {
shared_ptr<T> p;
SharedPtr() = default;
SharedPtr(SharedPtr &&other): p (std::move(other.p)) {
printf("SharedPtr. other: %p. this: %p\n", other.p.get(), this->p.get());
}
};
int main() {
shared_ptr<C1> _c1 = make_shared<C1>();
SharedPtr<C1> c1;
c1.p = std::move(_c1);
printf("c1.p: %p; _c1.p: %p\n", c1.p.get(), _c1.get());
c1.p->Close().finally([c11 = std::move(c1)] { // #1
printf("c11.p: %p\n", c11.p.get());
});
}
在 #1
处, 表达式多次使用了 c1, 而且有读有写, 这让我不由自主地想到了表达式求值顺序问题. 主要是 15 年有段时间为了搞懂 C++11 新引入的 memory order 这些概念痴迷于 C++ 标准文档中 sequenced before 这些概念, 养成直觉反应了. 在 #1
处, c1 = std::move(c1)
的求值是有可能 sequenced before c1.p->Close()
的, 即可能会先执行 c1 = std::move(c1)
再执行 c1.p->Close()
; 另外 shared_ptr::move() 是会清空 c1, 使得 c1 等同于 nullptr; 那么在执行 c1.p->Close()
不就跪了么=!
但实测了一下, 并没有跪, 如上程序会输出:
c1.p: 0x11d6ec0; _c1.p: (nil)
this: 0x11d6ec0
SharedPtr. other: (nil). this: 0x11d6ec0
c11.p: 0x11d6ec0
换个角度下, hologres 线上那么多实例都在正常运行着, 所以这段代码肯定不会有问题的啊== 但我不确定这个只是编译器自身的行为, 还是 C++ 标准定义的行为; 如果是前者, 那也是个隐藏炸弹啊, 排雷要尽早嘛. 就去翻了下 C++20 标准, 最终确定是 C++ 标准定义的行为. 自 C++17 之后, 对于表达式 a(expA).b(expB).c(expC)
, expA
is evaluated before calling b()
.
实际上, 如果找个尚不支持 C++17, 比如在 godbolt 上使用 gcc6.1 编译运行下就会得到如下输出:
c1.p: 0x1f1cec0; _c1.p: (nil)
SharedPtr. other: (nil). this: 0x1f1cec0
this: (nil) # BUG!
c11.p: 0x1f1cec0
而 hologres 为了使用 C++20 coroutine 等功能, 已经全面使用了 C++20 标准, 所以自然就没有问题咯==