允许 class 的用户移动私有成员

Allowing users of a class to move private members

说我有这个 class:

class Message
{
public:
    using Payload = std::map<std::string, boost::any>;

    Message(int id, Payload payload)
    : id_(id),
      payload_(std::move(payload))
    {}

    int id() const {return id_;}

    const Payload& payload() const {return payload_;}

private:
    int id_;
    Payload payload_;
};

其中 Payload 可能会很大且复制成本高。

我想为这个 Message class 的用户提供移动负载的机会,而不必复制它。执行此操作的最佳方法是什么?

我可以想到以下方法:

  1. 添加一个 Payload& payload() 重载,returns 一个可变引用。然后用户可以:

    Payload mine = std::move(message.payload())

  2. 别再假装我封装了payload_,就当成public成员吧

  3. 提供一个takePayload成员函数:

    Payload takePayload() {return std::move(payload_);}

  4. 提供这个备用成员函数:

    void move(Payload& dest) {dest = std::move(payload_);}

  5. (由 Tavian Barnes) Provide a payload getter overload that uses a ref-qualifier 提供:

    const Payload& payload() const {return payload_;}

    Payload payload() && {return std::move(payload_);}

备选方案 #3 似乎是 std::future::get 中所做的,重载 (1)。

任何有关最佳替代方案(或另一种解决方案)的建议都将不胜感激。


编辑:这是我正在努力完成的一些背景知识。在我的实际工作中,这个 Message class 是一些通信中间件的一部分,包含了一堆用户可能感兴趣或不感兴趣的其他元数据。我原以为用户可能想移动payload数据到自己的数据结构中,收到后丢弃原来的Message对象。

似乎最一致的方法是使用选项 5:

  1. 似乎不​​建议第一个和第二个选项,因为它们公开了实现细节。
  2. 当使用 Message 右值时,例如,函数中的 return,应该直接移动有效负载,它使用的是第 5 个选项:

    Messsage some_function();
    Payload playload(some_function().payload()); // moves
    
  3. 在表达式中使用 std::move(x) 通常表示 x 的值不依赖于前进,其内容可能已被转移。第5个选项与该符号一致。

  4. 使用相同的名称并让编译器确定内容是否可以移动使得在通用上下文中更容易:

    template <typename X>
    void f(X&& message_source) {
        Payload payload(message_source.get_message());
    }
    

    根据get_message()产生的是左值还是右值,负载被适当地复制或移动。第三个选项不会产生这种好处。

  5. 返回值可以在复制省略避免进一步潜在复制或移动的上下文中使用获得的值:

    return std::move(message).payload(); // copy-elision enabled
    

    这是第四个选项没有产生的东西。

在天平的负端 sheet 很容易错误地尝试移动负载:

return std::move(message.payload()); // whoops - this copies!

请注意,第 5 个选项的其他重载需要以不同方式声明:

Payload        payload() &&     { return std::move(this->payload_); }
Payload const& payload() const& { return this->payload_; }
          // this is needed --^

我建议的第一件事是如果可以的话,根本不要这样做。允许您的私有数据成员从中断封装中移出(甚至比 returning 对它们的 const 引用更糟糕)。在我的 ~35,000 行代码库中,我需要精确地执行一次。

话虽这么说,我确实需要做一次,而且这样做有可衡量的显着性能优势。以下是我对您建议的每个方法的看法:

  1. Add a Payload& payload() overload which returns a mutable reference. Users can then do:

    Payload mine = std::move(message.payload())

这里的缺点是用户可以做 message.payload() = something else;,这会扰乱你的不变量。

  1. Stop pretending that I'm encapsulating payload_ and just make it a public member.

封装不是孤注一掷的事情。在我看来,您应该尽可能多地进行封装,或者至少是合理的。

  1. Provide a takePayload member function:

Payload takePayload() {return std::move(payload_);}

如果您无法访问引用限定符(严重 VC++?),这是我最喜欢的解决方案。我可能会将其命名为 movePayload()。有些人甚至可能更喜欢它而不是选项 5,因为它更明确且不那么令人困惑。

  1. Provide this alternate member function:

void move(Payload& dest) {dest = std::move(payload_);}

当 return 值可以时为什么要使用 out 参数?

  1. (Courtesy of Tavian Barnes) Provide a payload getter overload that uses a ref-qualifier:

const Payload& payload() const {return payload_;}

Payload payload() && {return std::move(payload_);}

不出所料,这是我最喜欢的建议 :)。请注意,您必须写

const Payload& payload() const& { return payload_; }
Payload payload() && { return std::move(payload_); }

(const& 而不是 const) 否则编译器会抱怨不明确的重载。

这个成语并非没有一些警告,尽管我相信斯科特迈耶斯在他的一本书中建议过它,所以它不会太糟糕。其中之一是调用者语法很奇怪:

Message message;
Payload payload = std::move(message).payload();

如果您需要从 Message 中移出两个值,那就更糟了;双 std::move() 会让不熟悉该模式的人感到困惑。

另一方面,这比其他方法更安全,因为您只能从 Message 被视为 xvalues 的位置移动。

有一个小的变化:

Payload&& payload() && { return std::move(payload_); }

我真的不确定哪个更好。由于复制省略,两者的性能应该是相同的。当 return 值被忽略时,它们确实有不同的行为。