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

108

Я новичок в C ++, но не новичок в программировании. Я пытаюсь изучить C ++ (c ++ 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 хода и ни одной копии.

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

Джек Уилсон
источник
9
Мета: Не могу поверить, что кто-то только что задал хороший вопрос о C ++. +1.
23
К вашему сведению, std::stringэто класс, а CreditCardне примитивный тип.
Крис
7
Из-за путаницы со строками, хотя и не связанной, вы должны знать, что строковый литерал,, "abc"имеет не тип std::string, не тип char */const char *, а тип const char[N](в данном случае N = 4 из-за трех символов и нуля). Это хорошее, распространенное заблуждение, чтобы не мешать.
Крис
10
@chuex: Джон Скит отвечает на все вопросы о C #, гораздо реже о C ++.
Стив Джессоп,

Ответы:

158

САМЫЙ ВАЖНЫЙ ВОПРОС ПЕРВЫЙ:

Есть ли лучшие практики отправки параметров на C ++, потому что я действительно считаю это, скажем так, нетривиальным

Если вашей функции необходимо изменить исходный передаваемый объект, чтобы после возврата вызова изменения этого объекта были видны вызывающему, тогда вы должны передать ссылку lvalue :

void foo(my_class& obj)
{
    // Modify obj here...
}

Если вашей функции не нужно изменять исходный объект и не нужно создавать его копию (другими словами, ей нужно только наблюдать за его состоянием), тогда вы должны передать ссылку lvalue наconst :

void foo(my_class const& obj)
{
    // Observe obj here
}

Это позволит вам вызывать функцию как с lvalues ​​(lvalues ​​- это объекты со стабильной идентичностью), так и с rvalues ​​(rvalue - это, например, временные объекты или объекты, от которых вы собираетесь перейти в результате вызова 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 ​​и к одной копии при передаче rvalue. Если функция должна хранить этот объект где - то, вы могли бы выполнить дополнительный шаг от него (например, в случае , foo()является функцией - членом , который должен хранить значение в элементе данных ).

Если ходы дорогие для объектов типа my_class, вы можете рассмотреть вопрос о перегрузке foo()и предоставить одну версию для lvalue (принимая ссылку lvalue на const) и одну версию для rvalue (принимая ссылку 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's 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 не могут связываться с lvalue. Более того, при привязке ссылки к объекту перемещение не выполняется : это просто привязка ссылки. Таким образом, ход будет только один .


Итак, основываясь на рекомендациях, приведенных в первой части этого ответа, если вас беспокоит количество генерируемых ходов, когда вы берете 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, и это означает , что мы имеем право двигаться от него).

Энди Проул
источник
Неправильно ли говорить, что для непримитивных типов параметров нормально всегда использовать версию шаблона с форвардом?
Джек Уилсон,
1
@JackWillson: Я бы сказал, что вам следует прибегать к шаблонной версии только тогда, когда у вас действительно есть проблемы с производительностью ходов. См. Этот мой вопрос , на который есть хороший ответ, для получения дополнительной информации. В общем, если вам не нужно делать копию, а нужно только наблюдать, возьмите ссылку на const. Если вам нужно изменить исходный объект, возьмите по ссылке non-`const. Если вам нужно сделать копию, а ходы дешевы, берите по стоимости, а затем двигайтесь.
Энди Проул
2
Думаю, в третьем примере кода переходить не нужно. Вы могли бы просто использовать objвместо того, чтобы делать локальную копию для перемещения, не так ли?
juanchopanza
3
@AndyProwl: Вы получили мой +1 час назад, но, перечитав ваш (отличный) ответ, я хотел бы отметить две вещи: а) по значению не всегда создается копия (если она не перемещается). Объект может быть построен на месте на сайте вызывающего абонента, особенно с RVO / NRVO, это даже работает чаще, чем можно было бы подумать. б) Обратите внимание, что в последнем примере std::forwardможет быть вызван только один раз . Я видел, как люди помещали его в циклы и т.д., и поскольку этот ответ увидят многие новички, ИМХО должен быть толстый ярлык «Предупреждение!», Чтобы помочь им избежать этой ловушки.
Daniel Frey
1
@SteveJessop: И помимо этого, есть технические проблемы (не обязательно проблемы) с функциями пересылки ( особенно конструкторы, принимающие один аргумент ), в основном тот факт, что они принимают аргументы любого типа и могут std::is_constructible<>игнорировать черту типа, если они не являются должным образом SFINAE- ограниченный - что может быть нетривиальным для некоторых.
Энди Проул
11

Во-первых, позвольте мне исправить некоторые детали. Когда вы говорите следующее:

будет 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)) {}
Р. Мартиньо Фернандес
источник
Проблема с версией шаблона: пользователь больше не может писать Account("",0,{brace, initialisation}).
ipc
@ipc ах, правда. Это действительно раздражает, и я не думаю, что существует простой масштабируемый обходной путь.
R. Martinho Fernandes
6

Во-первых, 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используется по ошибке. Найдите универсальные ссылки .

Джозеф Мэнсфилд
источник
1

Я использую довольно простое правило для общего случая: используйте копию для 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; 
};

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

Дэвид Флери
источник
2
«Я использую довольно простое правило для общего случая: используйте копию для POD (int, bool, double, ...) и const & для всего остального». No. Just No.
Shoe
Может быть добавлено, если вы хотите изменить значение с помощью & (без const). Иначе я не вижу ... простоты достаточно. Но если вы так сказали ...
Дэвид Флери
Иногда вы хотите изменить примитивные типы по ссылке, а иногда вам нужно сделать копию объектов, а иногда вам нужно переместить объекты. Вы не можете просто сократить все до POD> по значению и UDT> по ссылке.
Shoe,
Хорошо, это то, что я добавил после. Может быть, это слишком быстрый ответ.
Дэвид Флери,
1

Для меня непонятно самое главное: передача параметров.

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

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

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

Обувь
источник