使用 std::optional 使我的 RAII 对象在移动构造函数/签名 [closed] 中失效

Using std::optional to invalidate my RAII object in move constructor/asignment [closed]

提问人:John O'brien 提问时间:6/1/2023 最后编辑:Adrian MoleJohn O'brien 更新时间:6/1/2023 访问量:142

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章用事实和引文来回答。

6个月前关闭。

假设我有一个 RAII 类,其实例永远不应该被复制:

class Session {

public:
    Session(); // Allocates a resource and sets generates a unique Session::id. 
    ~Session(); // Frees the resource

    Session(const Session&) = delete;
    Session& operator = (Session&) = delete;

private:
  std::uint32_t id;
}

鉴于我不能允许复制,我想允许移动,所以我需要实现移动构造函数和移动赋值运算符,但我不确定我应该如何处理移动的实例。Session::id

我可以:

  1. 将其设置为某个已知无效的值(可能将类型更改为 signed int 并用作无效值)-1
  2. 使用 类似 并将其设置为使其失效。std::optionalstd::nullopt

这些选项中哪一个(如果有的话)是正确的?

C++ C++20 RAII 标准可选

评论

0赞 apple apple 6/1/2023
您也可以只设置一个 .bool
0赞 digito_evo 6/1/2023
该决定是程序语义的一部分。这也取决于你希望如何优化代码。将浪费 4 个字节用于其中的布尔成员。我会说,如果它使代码更习惯并且更容易与之交互(对于客户),那么请选择这种方法。std::optional<std::uint32_t>std::optional
1赞 fabian 6/1/2023
请使用 ,除非创建的逻辑已经具有指示错误的返回值。大多数带有 C 接口的库都会带有这样的返回值,并且可能比 ...std::optionalidif(id != InvalidId)if (!id.has_value())
1赞 apple apple 6/1/2023
fwiw 我实际上反对在这里使用,因为没有什么真正可选std::optional
3赞 Remy Lebeau 6/1/2023
旁注:复制赋值运算符的参数应该是 ,就像在复制构造函数中一样,例如:constSession& operator = (const Session&) = delete;

答:

6赞 Ted Lyngmo 6/1/2023 #1

由于这可以通过牺牲 232 个可能值中的一个值来完成,因此我确实希望并初始化一个 unsigned 类型的常量。std::optionalidstd::string::nposid-1

例:

class Session {
public:
    // the constant used to signal an invalid id:
    static constexpr std::uint32_t invalid_id = static_cast<std::uint32_t>(-1);

    Session() :
        id(id_counter++)
        /* allocate the resource */
    {}
    Session(const Session&) = delete;
    Session(Session&& other) noexcept :
        id(std::exchange(other.id, invalid_id)) // exchange with the invalid id
        /* move or exchange the resource */
    {}
    Session& operator=(const Session&) = delete;
    Session& operator=(Session&& other) noexcept {
        // check for self-assignment if needed
        std::swap(id, other.id);
        // swap the resource
        return *this;
    }
    ~Session() {
        if(id != invalid_id) { // if the check is even needed
            /* free the resource */
        }
    }

private:
    inline static std::uint32_t id_counter = 0;
    std::uint32_t id;
};

演示

一种选择是让 be 如果这在您的情况下感觉更自然。您只需要初始化并将 id 计数更改为 .0invalid_idinvalid_id0id(++id_counter)

1赞 digito_evo 6/1/2023 #2

只有当班级只有一个成员时,我才会选择这种方法。因此,在这种情况下,有一个成员来指示类实例的有效性似乎没问题。std::optionalbool

使用该成员,您可以实现类的重载。这样,表达式 like 是可能的(而不是写 or 等)。booloperator bool()if (session_instance)if (session_instance.id != -1)if (session_instance.id.has_value())

举个例子:

#include <utility>
#include <cstdint>
#include <cstddef>
#include <cstdio>

class Session {

public:
    explicit Session(const std::uint32_t id, const std::size_t size = 10)
    : is_valid { true }, m_id { id }, m_buffer { new char[size] }
    {
        m_buffer[size - 1] = '\0';
    }
    ~Session()
    {
        if ( m_buffer != nullptr )
            delete[] m_buffer;
    }

    Session(const Session&) = delete;
    Session& operator = (const Session&) = delete;

    Session(Session&& rhs) noexcept
    : is_valid { std::exchange(rhs.is_valid, false) },
      m_id { std::exchange(rhs.m_id, 0) },
      m_buffer { std::exchange(rhs.m_buffer, nullptr) }
    {}
    Session& operator = (Session&& rhs) noexcept
    {
        if ( this != &rhs )
        {
            is_valid = std::exchange(rhs.is_valid, false);
            m_id = std::exchange(rhs.m_id, 0);
            if ( m_buffer != nullptr )
                delete[] m_buffer;
            m_buffer = std::exchange(rhs.m_buffer, nullptr);
        }

        return *this;
    }

    operator bool () const noexcept
    {
        return is_valid;
    }

private:
    bool is_valid;
    std::uint32_t m_id;
    char* m_buffer { nullptr };
};

int main( )
{
    Session ses { 1000, 10 };
    Session ses2 { 100, 20 };
    ses2 = std::move( ses );

    if ( ses )
        std::printf( "ses is valid\n");
    else
        std::printf( "ses is NOT valid\n");
}

注意:如果将类型更改为 ,也可以实现 。那么就不需要额外的成员了。您可以简单地将成员变量添加到您的类中,例如并在 .operator bool()return m_id != -1;m_idintboolinline static constexpr int invalid_id { -1 };m_idoperator bool()

评论

1赞 Ted Lyngmo 6/1/2023
“而不是写 if (session_instance.id != -1)” - 如果需要该运算符,则会出现,因此在这方面与用户的接口不会有所不同。operator bool () const noexcept { return id != invalid_id; }
0赞 digito_evo 6/1/2023
@TedLyngmo 是的,对。要编辑它。
2赞 Ted Lyngmo 6/1/2023
另一个吹毛求疵:应该只是因为已经在内部进行了此检查。if ( m_buffer != nullptr ) delete[] m_buffer;delete[] m_buffer;delete[]nullptr
2赞 Jeff Garrett 6/1/2023 #3

std::optional是一个糟糕的拟合,因为从可选移的不是脱离的。(示例)

由于其默认移动行为与所需的语义不匹配,因此仍必须提供自定义移动操作,并且没有多大价值。std::optional

可以编写一个类似可选的类型,该类型具有所需的移动操作。这种方法的一个优点是可以将默认的未使用值指定为模板参数。这种方法的一个缺点是类有多个成员,它们可能都有效或无效。因此,在更正移动操作后,使用类似可选的类型将导致将所有类状态移动到另一种类型。这是一个相当大的负担。

另一方面,可以很容易地编写一个类似标志的类,以指示实例是否已从中移动

class engaged {
public:
    constexpr engaged() noexcept = default;
    constexpr engaged(const engaged&) noexcept = default;
    constexpr engaged& operator=(const engaged&) noexcept = default;
    constexpr engaged(engaged&& other) noexcept : engaged_(std::exchange(other.engaged_, false)) { }
    constexpr engaged& operator=(engaged&& other) noexcept { engaged_ = std::exchange(other.engaged_, false); return *this; }
    constexpr explicit operator bool() const noexcept { return engaged_; }
private:
    bool engaged_ = true;
};

()

你会怎么做?

需要权衡取舍。RAII 类型通常具有多个整数。这样的类型通常也不多。它是否已移出可能无关紧要。一般来说,我会专注于编写哪些 RAII 类型以及每种类型的适当接口。