C++表达式求值顺序的一个小问题

Posted by w@hidva.com on November 22, 2021

在学习 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 标准, 所以自然就没有问题咯==