Ответ 1
САМЫЙ ВАЖНЫЙ ВОПРОС:
Есть ли какие-либо рекомендации по отправке параметров в C++, потому что я действительно нахожу это, допустим, не тривиальным
Если вашей функции необходимо изменить исходный объект, который будет передан, чтобы после того, как вызов вернется, модификации этого объекта будут видны вызывающему, тогда вы должны пройти по ссылке lvalue:
void foo(my_class& obj)
{
// Modify obj here...
}
Если вашей функции не нужно изменять исходный объект и не нужно создавать ее копию (другими словами, она должна только наблюдать за ее состоянием), тогда вы должны передать ссылку lvalue на const
:
void foo(my_class const& obj)
{
// Observe obj here
}
Это позволит вам вызвать функцию как с lvalues (lvalues - объекты со стабильным идентификатором), так и с rvalues (rvalues - это, например, временные или объекты, из которых вы собираетесь переместиться, в результате вызова std::move()
).
Можно также утверждать, что для фундаментальных типов или типов, для которых быстрое копирование, например int
, bool
или char
, нет необходимости передавать по ссылке, если функция просто должна наблюдать за значением, а передача по значению должна быть предпочтительной, Это правильно, если ссылочная семантика не нужна, но что, если функция захотела сохранить указатель на тот же самый входной объект где-то, так что в будущем читается через этот указатель, будут видеть изменения значений, которые были выполнены в некоторой другой части код? В этом случае переход по ссылке является правильным решением.
Если вашей функции не требуется изменять исходный объект, но необходимо сохранить копию этого объекта (возможно, чтобы вернуть результат преобразования ввода без изменения ввода), вы можете рассмотреть возможность принятия по значению:
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
Вызов указанной выше функции всегда будет приводить к одной копии при передаче lvalues и в одном перемещении при передаче rvalues. Если ваша функция должна где-то хранить этот объект, вы можете выполнить дополнительный переход от него (например, в случае, если foo()
является функцией-членом, которая должна хранить значение в элементе данных).
В случае, если ходы дороги для объектов типа my_class
, вы можете рассмотреть возможность перегрузки foo()
и предоставить одну версию для lvalues (принятие ссылки lvalue для const
) и одну версию для rvalues (принятие ссылки rvalue):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
Вышеупомянутые функции настолько похожи, что вы можете сделать одну из них: foo()
может стать шаблоном функции, и вы можете использовать совершенную пересылку для определения того, будет ли перемещаться или копироваться передаваемый объект внутренне сгенерированный:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
Вы можете больше узнать об этом дизайне, просмотрев этот разговор Скотта Мейерса (просто помните, что термин "Универсальные ссылки", который он использует, нестандартен).
Следует иметь в виду, что std::forward
обычно заканчивается перемещением для rvalues, поэтому, хотя он выглядит относительно невинным, перенаправление одного и того же объекта несколько раз может быть источником проблем - например, перемещение от одного и того же объект дважды! Поэтому будьте осторожны, чтобы не помещать это в цикл, и не пересылать один и тот же аргумент несколько раз в вызове функции:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Также обратите внимание, что вы обычно не используете шаблонное решение, если у вас нет веских оснований для этого, так как это затрудняет чтение вашего кода. Обычно вам следует сосредоточиться на ясности и простоте.
Вышеприведенные простые инструкции, но в большинстве случаев они указывают на хорошие дизайнерские решения.
В ОТНОШЕНИИ ОТДЫХА ВАШЕГО ПОЧТА:
Если я переписал его как [...], будет 2 хода и нет копии.
Это неверно. Начнем с того, что ссылка rvalue не может привязываться к lvalue, поэтому она будет компилироваться только тогда, когда вы передаете rvalue типа CreditCard
вашему конструктору. Например:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that also an rvalue)
Account acc("asdasd",345, std::move(cc));
Но это не сработает, если вы попытаетесь это сделать:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Поскольку cc
- это lvalue, а ссылки rvalue не могут связываться с lvalues. Более того, при привязке ссылки к объекту никакого перемещения не выполняется: это просто привязка ссылки. Таким образом, будет только один шаг.
Поэтому, основываясь на рекомендациях, приведенных в первой части этого ответа, если вы заинтересованы в количестве генерируемых ходов при приеме кредитной CreditCard
по значению, вы можете определить две перегрузки конструктора, один из которых принимает ссылку lvalue для const
(CreditCard const&
), а другой - с помощью ссылки rvalue (CreditCard&&
).
Разрешение перегрузки будет выбирать первое при передаче lvalue (в этом случае будет выполнена одна копия), а вторая при передаче rvalue (в этом случае будет выполнен один шаг).
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
Ваше использование std::forward<>
обычно рассматривается, когда вы хотите добиться идеальной пересылки. В этом случае ваш конструктор будет фактически шаблоном конструктора и будет выглядеть более или менее следующим образом
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
В некотором смысле это сочетает в себе как перегрузки, которые я показал ранее, в одну единственную функцию: C
будет выведена как CreditCard&
в случае, если вы передаете значение lvalue, и из-за правил сбрасывания ссылок это приведет к созданию этой функции:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Это приведет к созданию копии creditCard
, как вы пожелаете. С другой стороны, когда передается rvalue, C
будет выведено как CreditCard
, и вместо этого будет создана эта функция:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Это вызовет перемещение-построение creditCard
, которое вы хотите (потому что переданное значение является rvalue, и это означает, что мы имеем право переходить от него).