Разрешить пользователям класса перемещать частных членов
Скажем, у меня есть этот класс:
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;
, что может испортить ваши инварианты.
- Перестаньте притворяться, что я инкапсулирую полезную нагрузку_ и просто делаю ее публичным.
Инкапсуляция не является ничего или ничего; ИМО вы должны стремиться к такому же количеству инкапсуляции, насколько это возможно или, по крайней мере, разумно.
- Предоставить функцию члена takePayload:
Payload takePayload() {return std::move(payload_);}
Это мое любимое решение, если у вас нет доступа к ref-qualifiers (серьезно VС++?). Я бы назвал его movePayload()
. Некоторые могут даже предпочесть вариант 5, потому что он более явный и менее запутанный.
- Предоставить эту альтернативную функцию-член:
void move(Payload& dest) {dest = std::move(payload_);}
Зачем использовать параметр out при возврате значения?
- (Предоставлено 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_); }
Я действительно не уверен, какой из них лучше. Из-за копирования, эффективность обоих должна быть одинаковой. Они имеют другое поведение, когда возвращаемое значение игнорируется.