【问题标题】:Allowing users of a class to move private members允许类的用户移动私有成员
【发布时间】:2015-06-16 19:47:15
【问题描述】:

假设我有这门课:

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 类的用户有机会移动有效负载,而不必复制它。这样做的最佳方法是什么?

我可以想到以下几种方式:

  1. 添加一个Payload&amp; payload() 重载,它返回一个可变引用。然后用户可以这样做:

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

  2. 不要再假装我正在封装payload_,而是将其设为公共成员。

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

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

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

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

  5. (由Tavian Barnes 提供)提供使用ref-qualifierpayload getter 重载:

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

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

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

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


编辑:以下是我想要完成的一些背景知识。在我的实际工作中,这个 Message 类是一些通信中间件的一部分,并且包含用户可能感兴趣或可能不感兴趣的一堆其他元数据。我曾认为用户可能希望将有效负载数据移动到他的或者她自己的数据结构,收到后丢弃原来的Message对象。

【问题讨论】:

  • “最佳”一般来说似乎有点主观。就可读性而言,备选方案#3 对我来说似乎是最好的。另外,为什么不为整个 Message 类实现/使用移动语义?
  • 我认为这在很大程度上取决于为什么用户想要移动(或者,事实上,甚至访问)该成员。为什么这个类不是所有者?
  • @Steephen 这个问题和这个有什么关系?这是从班级成员那里转移过来的;它必须是明确的。
  • 另一个选择是Payload payload() &amp;&amp; {return std::move(payload_); }
  • @Steephen:我相信另一个问题,其动机是防止临时复制。在我的情况下,我故意希望用户能够移动私有成员变量,以便它从最初托管它的对象中永久“消失”。

标签: c++ c++11 move encapsulation


【解决方案1】:

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

  1. 似乎不建议使用第 1 和第 2 选项,因为它们会暴露实现细节。
  2. 当使用Message 右值时,例如,从函数返回时,应该直接移动有效负载并且它使用第 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
    

    这是第四个选项所没有的。

在资产负债表的不利方面,很容易错误地尝试移动有效载荷:

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 --^

【讨论】:

  • 如果我理解正确,使用选项 5,客户端应该能够做到这一点,Payload mine = std::move(message).payload(); 这将有效地将有效负载从message 移动到mine。这个例子实际上与你在项目#2 中的例子中发生的事情相同。这是正确的吗?
  • @EmileCormier:假设在payload 之后有一个():是的。
  • 糟糕,是的。在payload 之后应该有一个()。修正了我的评论。
  • Dietmar,在没有 ref-qualifier 支持的情况下(例如,在 VS2013 上),你认为我的选项 #3 会是“万恶之源”吗?
  • 这是一个不同的问题...根据需要,第四个选项可能会更好,因为它为通用案例提供了一致的界面。如果只返回一个对象,实际上仍然可以省略副本,尽管有必要创建一个正在重置的Payload 对象。
【解决方案2】:

我建议的第一件事是如果你能提供帮助,请不要这样做。允许您的私有数据成员从中断封装中移出(甚至比返回对它们的 const 引用更糟糕)。在我的大约 35,000 行代码库中,我需要精确地执行一次。

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

  1. 添加返回可变引用的 Payload& payload() 重载。然后用户可以这样做:

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

这里的缺点是用户可以使用message.payload() = something else;,这可能会弄乱您的不变量。

  1. 别再假装我在封装payload_,而是让它成为公共成员。

封装不是全有或全无的事情。 IMO,您应该尽可能多地进行封装,或者至少是合理的。

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

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

如果您无权访问引用限定符(说真的 VC++?),这是我最喜欢的解决方案。我可能会将其命名为movePayload()。有些人甚至可能更喜欢它而不是选项 5,因为它更明确且不易混淆。

  1. 提供此备用成员函数:

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

当有返回值时为什么要使用 out 参数?

  1. (由 Tavian Barnes 提供)提供有效载荷 getter 重载, 使用 ref 限定符:

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

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

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

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

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

这个成语并非没有一些注意事项,尽管我相信 Scott Meyers 在他的一本书中提出了这一点,所以它不会太糟糕。其中之一是调用者语法很奇怪:

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

如果您需要将两个值移出Message,那就更糟了;对于不熟悉该模式的人来说,双重 std::move() 会非常混乱。

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

有一个小的变化:

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

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

【讨论】:

  • 我想听从您的警告,根本不要这样做,但这是针对库的,我不想规定它们应该如何处理消息有效负载。我也不想将复制成本强加给他们。幸运的是,一旦他们收到了这个Message 对象,他们就可以对它做任何他们想做的事情,而且完全不会影响通信中间件。编写库的糟糕之处在于你不能对使用模式做出广泛的假设!
  • 虽然我相信 Scott Meyers 在他的一本书中提出了建议,所以它不会太糟糕...我在 Effective Modern C++ 的第 12 条中找到了它。
猜你喜欢
  • 1970-01-01
  • 2013-02-13
  • 2012-05-03
  • 1970-01-01
  • 2021-12-30
  • 2013-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多