std::launder 的正确用法

5

我希望确认我是否正确使用std::launder(...),以确保我正确理解其用法。

我正在创建一个基于 Rust 实现的 C++ Result<U,E>

template <typename E>
class ResultStorage<void, E, std::enable_if_t<std::is_trivially_destructible_v<E>>> {
    using type = typename std::aligned_storage<sizeof(E), alignof(E)>::type;

public:
    explicit constexpr ResultStorage(const Ok<void>&) noexcept : tag_(ResultTag::OK) {}

    explicit constexpr ResultStorage(const Ok<void>&&) noexcept : tag_(ResultTag::OK) {}

    explicit constexpr ResultStorage(const Err<E>& err) noexcept(std::is_nothrow_copy_constructible<E>())
        : tag_(ResultTag::ERR) {
        new (&error_) E(err.get_error());
    }
    explicit constexpr ResultStorage(const Err<E>&& err) noexcept(std::is_nothrow_move_constructible<E>())
        : tag_(ResultTag::ERR) {
        new (&error_) E(std::move(err.get_error()));
    }

    ~ResultStorage() = default;

    [[nodiscard]] constexpr E& get_error() & noexcept {
        assert_err(tag_);
        return *std::launder(reinterpret_cast<E*>(&error_));
    }
    
    // Code omitted for brevity
private:
    ResultTag tag_;
    type error_;

    template <typename Rv, typename Ev>
    friend class result::Result;
};

在我的代码中,我使用using type = typename std::aligned_storage<sizeof(E), alignof(E)>::type;作为我的存储类型。 我认为当我从函数中返回错误类型时,需要使用std::launder(...),如下所示:
    [[nodiscard]] constexpr E& get_error() & noexcept {
        assert_err(tag_);
        return *std::launder(reinterpret_cast<E*>(&error_));
    }

我相信我需要使用 std::launder(...) 是因为传入的错误类型可能是一个结构体,可能具有 const 值,如果不使用 std::launder(...) ,则在第一次初始化时它将引用 const 成员值。如果我要重复使用该分配的存储,它将始终引用最初的 const 成员值。
我对 std::launder 有一个初步的了解,所以需要解释在什么情况下需要使用它。我已经查看了cppreference中有关此函数的信息,但仍然觉得难以理解。
注意:完整的实现可以在 GitHub 上找到。

1
我认为在你的情况下,error_ 类型可以是 std::optional<E>。这将极大地简化你的设计。 - bolov
是的,我不反对使用 std::optional<E> 会更简单。但我没有这样做,因为我想尽可能少地依赖标准库,尝试从零开始实现一些东西。我当然考虑过使用 std::optional<E>,但我认为这将是一个有趣的方式来实现它并对结果进行分析。编辑:最终它确实引发了这个问题,所以我认为从学习的角度来看,它达到了我想要的效果。 - cogle
可以使用std :: byte替换std :: aligned_storage吗? - bolov
1个回答

3

我不会对std::launder进行猜测,但是您的代码还有另一个问题,解决它将使得std::launder不再需要:

new (&error_) E(err.get_error());
(reinterpret_cast<E*>(&error_);

这是错误的,因为标准只保证对于标准布局类型,创建的对象实际上将从&error 开始。我知道实际情况下不成立的一个特例是多重继承的情况。
因此,正确的方式是:
// class data member:
E* ptr = nullptr;

// then:
ptr = new (&error_) E(err.get_error());

只使用 ptr 来访问存储的对象。这样也就不再需要使用 std::launder 了。

当你在 C++ 中降到这个低级别时,会遇到许多微妙的陷阱。


感谢您指出这一点,在重新阅读有关std::aligned_storage的页面时,我发现代码中还存在另一个潜在的错误。我将专门针对std::is_standard_layout进行特化,并仅在其为真时使用问题中的代码。 - cogle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接