Как правильно передать параметры?

Я начинающий С++, но не начинающий программист. Я пытаюсь изучить С++ (С++ 11), и это для меня неясное самое главное: прохождение параметров.

Я рассмотрел эти простые примеры:

  • Класс, который имеет все его элементы примитивных типов:
    CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)

  • Класс, который имеет в качестве членов примитивные типы + 1 сложный тип:
    Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)

  • Класс, который имеет в качестве членов примитивные типы + 1 набор некоторого сложного типа: Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

Когда я создаю учетную запись, я делаю это:

    CreditCard cc("12345",2,2015,1001);
    Account acc("asdasd",345, cc);

Очевидно, что кредитная карта будет скопирована дважды в этом сценарии. Если я переписываю этот конструктор как

Account(std::string number, float amount, CreditCard& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(creditCard)

будет один экземпляр. Если я переписал его как

Account(std::string number, float amount, CreditCard&& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(std::forward<CreditCard>(creditCard))

Будет сделана 2 хода и нет копии.

Я думаю, иногда вам может понадобиться скопировать некоторый параметр, иногда вы не хотите копировать, когда вы создаете этот объект.
Я пришел из С# и, будучи использованным для ссылок, мне немного странно, и я думаю, что для каждого параметра должно быть 2 перегрузки, но я знаю, что я ошибаюсь.
Есть ли какие-либо рекомендации о том, как отправлять параметры на С++, потому что я действительно нахожу его, допустим, не тривиальным. Как бы вы справились со своими примерами, представленными выше?

Ответ 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, и это означает, что мы имеем право переходить от него).

Ответ 2

Сначала позвольте мне исправить некоторые подробности. Когда вы скажете следующее:

будет 2 хода и нет копии.

Это неверно. Привязка к ссылке rvalue не является движением. Есть только один ход.

Кроме того, поскольку CreditCard не является параметром шаблона, std::forward<CreditCard>(creditCard) является просто верным способом сказать std::move(creditCard).

Теперь...

Если ваши типы имеют "дешевые" ходы, вы можете просто сделать свою жизнь легкой и взять все по значению и "std::move вдоль".

Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
  amount(amount),
  creditCard(std::move(creditCard)) {}

Этот подход даст вам два хода, если он может дать только один, но если ходы дешевы, они могут быть приемлемыми.

Пока мы говорим об этом "дешевом ходу", я должен напомнить вам, что std::string часто реализуется с так называемой оптимизацией небольших строк, поэтому его действия могут быть не такими дешевыми, как копирование некоторых указателей. Как обычно, с проблемами оптимизации, важно ли это или нет, спросить своего профилировщика, а не меня.

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

Если есть только один проблемный параметр, вы можете предоставить две перегрузки с T const& и T&&. Это будет связывать ссылки все время до фактической инициализации члена, где происходит копирование или перемещение.

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

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

template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
  amount(amount),
  creditCard(std::forward<TCreditCard>(creditCard)) {}

Ответ 3

Прежде всего, std::string - довольно классный тип класса, как и std::vector. Это, конечно, не примитивно.

Если вы делаете какие-либо большие перемещаемые типы по значению в конструктор, я бы std::move их в член:

CreditCard(std::string number, float amount, CreditCard creditCard)
  : number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }

Именно так я бы рекомендовал реализовать конструктор. Это заставляет элементы number и creditCard перемещаться, а не копировать. Когда вы используете этот конструктор, будет одна копия (или перемещение, если она временно), когда объект передается в конструктор, а затем один шаг при инициализации члена.

Теперь рассмотрим этот конструктор:

Account(std::string number, float amount, CreditCard& creditCard)
  : number(number), amount(amount), creditCard(creditCard)

Вы правы, это будет одна копия creditCard, потому что она сначала передается конструктору по ссылке. Но теперь вы не можете передать объекты const в конструктор (потому что ссылка не const), и вы не можете передавать временные объекты. Например, вы не могли этого сделать:

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

Теперь рассмотрим:

Account(std::string number, float amount, CreditCard&& creditCard)
  : number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))

Здесь вы указали непонимание ссылок на rvalue и std::forward. Вы действительно должны использовать std::forward, когда объект, который вы пересылаете, объявляется как T&& для некоторого выведенного типа T. Здесь creditCard не выводится (я предполагаю), и поэтому std::forward используется по ошибке. Посмотрите универсальные ссылки.

Ответ 4

Это для меня неясное самое главное: прохождение параметров.

  • Если вы хотите изменить переменную, переданную внутри функции/метода
    • вы передаете его по ссылке
    • вы передаете его как указатель (*)
  • Если вы хотите прочитать значение/переменную, переданную внутри функции/метода
    • вы передаете его по ссылке const
  • Если вы хотите изменить значение, переданное внутри функции/метода
    • вы его обычно передаете, копируя объект (**)

(*) указатели могут ссылаться на динамически распределенную память, поэтому, когда это возможно, вы должны предпочесть ссылки на указатели, даже если ссылки, в конце концов, обычно реализуются как указатели.

(**) "обычно" означает конструктор копирования (если вы передаете объект того же типа параметра) или обычным конструктором (если вы передаете совместимый тип для класса). Например, когда вы передаете объект как myMethod(std::string), конструктор копирования будет использоваться, если ему передается std::string, поэтому вы должны убедиться, что он существует.

Ответ 5

Я использую довольно простое правило для общего случая: Используйте копию для POD (int, bool, double,...) и const и для всего остального...

И, желая скопировать или нет, на подпись метода не отвечает, но больше того, что вы делаете с параметрами.

struct A {
  A(const std::string& aValue, const std::string& another) 
    : copiedValue(aValue), justARef(another) {}
  std::string copiedValue;
  const std::string& justARef; 
};

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