std::expected 的这种仅移动类型的行为是 MSVC bug 还是未定义的行为?

Is this behavior of std::expected with move-only types a MSVC bug or undefined behavior?

提问人:dw218192 提问时间:11/13/2023 更新时间:11/13/2023 访问量:102

问:

使用 Microsoft Visual Studio 的 MSVC 编译器进行编译时,我的 C++ 代码遇到了一个奇怪的问题,我正在尝试根据 C++ 标准确定它是编译器错误还是未定义的行为。

最小可重现的例子如下:

#include <utility>
#include <iostream>
#include <memory>
#include <expected>

struct UniqueHandle {
    UniqueHandle() { 
        val = nullptr; 
    }
    ~UniqueHandle() { 
        std::cout << "~UniqueHandle(" << val << ")" << std::endl; 
    }
    UniqueHandle(void* val) : val(val) {}
    UniqueHandle(UniqueHandle&& other) { 
        std::swap(val, other.val);
    }
    UniqueHandle& operator=(UniqueHandle&& other) {
        std::swap(val, other.val);
        return *this;
    }
    void* val;
};
std::expected<UniqueHandle, int> foo() {
    return UniqueHandle {};
}
int main() {
    auto exp_res = foo();
    UniqueHandle result {};
    if (exp_res) {
        result = std::move(exp_res.value());
    }
    return 0;
}

该程序在调试版本中使用 VS 2022 工具链生成以下输出:

~UniqueHandle(CCCCCCCCCCCCCCCC)
~UniqueHandle(0000000000000000)
~UniqueHandle(0000000000000000)

在发布版本中,第一行是一些垃圾值。我看不出这个值是从哪里来的。但是,当由 g++ 编译时,它会按预期工作,这里是实时示例:

https://gcc.godbolt.org/z/zMh5W6MMj

任何见解或解释将不胜感激!

C 可视化-C++ C++23 标准预期

评论

0赞 Sam Varshavchik 11/13/2023
这看起来像一个编译器错误。
0赞 Sash Sinha 11/13/2023
叮当声会发生什么?
7赞 tkausl 11/13/2023
I fail to see where this value comes from.从损坏的移动构造函数实现中。 在与其他交换之前,您永远不会初始化它。UniqueHandle(UniqueHandle&& other) { std::swap(val, other.val); }valval
0赞 ChrisMM 11/13/2023
0xCCCCCCCCCCCCCCCCMSVC 用于显示未初始化的内存。
1赞 StoryTeller - Unslander Monica 11/13/2023
提示:使用 std::exchange 来表示移动的东西。它有助于避免错误。

答:

3赞 Alan Birtles 11/13/2023 #1

在 move 中,构造函数未初始化。因此,设置为此未初始化的值。MSVC 的调试运行时将未初始化的内存设置为 0xCCCCCCCC,因此这是打印的值。您可以通过添加成员初始值设定项来查看 GCC 中的相同行为: https://gcc.godbolt.org/z/hMa9Ka9cbthis->valstd::swapother.val

可以使用 std::exchange 实现移动构造函数,以帮助避免此问题:

UniqueHandle(UniqueHandle&& other): val{std::exchange(other.val, nullptr)} { 
}

通过始终使用成员初始值设定项,可以更清楚地看到所有成员在使用之前都已初始化,并且显式声明了从成员中移动的新值。

1赞 Red.Wave 11/13/2023 #2

公认的答案是完美的解决方案。 但是在一般情况下,我会使用 default+swap idiom(我编造的成语):

void UniqueHandler::swap(UniqueHandler&);
UniqueHandler::UniqueHandler(UniqueHandler&& rvalue)
: UniqueHandler{/*delegate to default*/}
{ this->swap(rvalue); }; //swap

这是委托构造函数的用例。