Search code examples
c++c++11moveencapsulation

Allowing users of a class to move private members


Say I have this 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_;
};

where Payload could potentially be large and expensive to copy.

I would like to give users of this Message class the opportunity to move the payload instead of having to copy it. What would be the best way to go about doing this?

I can think of the following ways:

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

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

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

  3. Provide a takePayload member function:

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

  4. Provide this alternate member function:

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

  5. (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_);}

Alternative #3 seems to be what's done in std::future::get, overload (1).

Any advice on what would be the best alternative (or yet another solution) would be appreciated.


EDIT: Here's some background on what I'm trying to accomplish. In my real life work, this Message class is part of some communications middleware, and contains a bunch of other meta data that the user may or may not be interested in. I had thought that the user might want to move the payload data to his or her own data structures and discard the original Message object after receiving it.


Solution

  • It seems the most consistent approach is to use option 5:

    1. The 1st and 2nd option don't seem to be advised as they expose implementation details.
    2. When using a Message rvalue, e.g., a return from a function, it should be straight forward to move the payload and it is using the 5th option:

      Messsage some_function();
      Payload playload(some_function().payload()); // moves
      
    3. Using std::move(x) with an expression conventially indicates that the value of x isn't being depended upon going forward and its content may have been transferred. The 5th option is consistent with that notation.

    4. Using the same name and having the compiler figure out whether the content can be moved makes it easier in generic contexts:

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

      Depending on whether get_message() yields an lvalue or an rvalue the payload is copied or moved appropriately. The 3rd option doesn't yield that benefit.

    5. Returning the value make it possible to use the obtained in a context where copy-elision avoids further potential copies or moves:

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

      This is something the 4th option doesn't yield.

    On the negative side of the balance sheet it is easy to incorrectly attempt moving the payload:

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

    Note, that the other overload for the 5th option needs be to be declared differently, though:

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