Разрешить пользователям класса перемещать частных членов

Скажем, у меня есть этот класс:

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 возможность переместить полезную нагрузку вместо копирования. Каким будет лучший способ сделать это?

Я могу думать о следующих путях:

  • Добавьте перегрузку Payload& payload(), которая возвращает изменчивую ссылку. Затем пользователи могут:

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

  • Прекратите делать вид, что я инкапсулирую payload_ и просто сделаю его публичным.

  • Предоставить функцию члена takePayload:

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

  • Предоставьте эту альтернативную функцию-член:

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

  • (Предоставлено Tavian Barnes) Предоставьте перегрузку Payload, которая использует ref-qualifier:

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

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

Альтернативой № 3, по-видимому, является то, что сделано в std:: future:: get, перегрузка (1).

Было бы полезно получить любые рекомендации о том, какая была бы лучшая альтернатива (или еще одно решение).


РЕДАКТИРОВАТЬ. Вот несколько примеров того, что я пытаюсь выполнить. В моей реальной жизни этот класс Message является частью некоторого связующего ПО для связи и содержит кучу других метаданных, которые пользователь может или не может заинтересовать. Я думал, что пользователь может захотеть перенести данные полезной нагрузки в свою или ее собственные структуры данных и отбросить исходный объект сообщения после его получения.

Ответ 1

Кажется, наиболее последовательным подходом является использование опции 5:

  • 1-й и 2-й опции, похоже, не рекомендуются, поскольку они раскрывают детали реализации.
  • При использовании значения Message rvalue, например возврата от функции, он должен быть прямым для перемещения полезной нагрузки и использует 5-й вариант:

    Messsage some_function();
    Payload playload(some_function().payload()); // moves
    
  • Использование std::move(x) с выражением conventially указывает, что значение x не зависит от продвижения вперед, и его содержимое могло быть перенесено. 5-й вариант соответствует этой нотации.

  • Используя одно и то же имя и имея компилятор, выясните, можно ли перемещать содержимое, упрощает его в общих контекстах:

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

    В зависимости от того, получает ли значение get_message() значение lvalue или rvalue, полезная нагрузка копируется или перемещается соответствующим образом. Третий вариант не дает этой выгоды.

  • Возврат значения позволяет использовать полученные в контексте, где копирование исключает дальнейшие потенциальные копии или перемещения:

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

    Это то, что 4-й вариант не дает.

На отрицательной стороне баланса легко выполнить попытку перемещения полезной нагрузки:

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

Ответ 2

Первое, что я бы рекомендовал, - не делать этого вообще, если вы можете помочь ему. Разрешить перемещение ваших частных членов данных из разрывов инкапсуляции (даже хуже, чем возврат ссылок на них). В моей ~ 35 000 строк кода мне нужно было сделать это ровно один раз.

Это, как говорится, мне нужно было сделать это один раз, и для этого были измеримые и значительные преимущества в производительности. Здесь мое мнение по каждому из предложенных вами подходов:

  • Добавить полезную нагрузку & loadload(), которая возвращает изменчивую ссылку. Затем пользователи могут:

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

Недостатком здесь является то, что пользователи могут делать message.payload() = something else;, что может испортить ваши инварианты.

  1. Перестаньте притворяться, что я инкапсулирую полезную нагрузку_ и просто делаю ее публичным.

Инкапсуляция не является ничего или ничего; ИМО вы должны стремиться к такому же количеству инкапсуляции, насколько это возможно или, по крайней мере, разумно.

  1. Предоставить функцию члена takePayload:

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

Это мое любимое решение, если у вас нет доступа к ref-qualifiers (серьезно VС++?). Я бы назвал его movePayload(). Некоторые могут даже предпочесть вариант 5, потому что он более явный и менее запутанный.

  1. Предоставить эту альтернативную функцию-член:

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

Зачем использовать параметр out при возврате значения?

  1. (Предоставлено Tavian Barnes) Обеспечить перегрузку полезной нагрузки, которая использует 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_); }

Я действительно не уверен, какой из них лучше. Из-за копирования, эффективность обоих должна быть одинаковой. Они имеют другое поведение, когда возвращаемое значение игнорируется.