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


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 #, і, використовуючи посилання, мені це трохи дивно, і я думаю, що для кожного параметра повинно бути 2 перевантаження, але я знаю, що я помиляюся.
Чи є найкращі практики, як надсилати параметри в C ++, тому що я дійсно вважаю це, скажімо, нетривіальним. Як би ви поставилися до моїх прикладів, поданих вище?


9
Мета: Я не можу повірити, що хтось просто задав хороший C ++ питання. +1.

23
FYI std::string- це клас, як CreditCardі не примітивний тип.
chris

7
Через плутанину рядків, хоч і не пов’язану між собою, ви повинні знати, що літеральний рядок, "abc"- це не типу std::string, не типу char */const char *, а типу const char[N](при цьому N = 4 у зв'язку із трьома символами та нулем). Це добре, поширене оману, щоб вийти з дороги.
chris

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 ​​(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...
}

Викликання вищевказаної функції завжди призведе до отримання однієї копії при передачі значень, а в одному - при переміщенні значень 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, як правило, в результаті руху оцінюються, тому, хоча це виглядає відносно невинне, пересилання одного і того ж об'єкта кілька разів може бути причиною неприємностей - наприклад, переміщення з одного і того ж об’єкта двічі! Тому будьте обережні, щоб не ставити це в цикл і не пересилати один і той же аргумент кілька разів у виклик функції:

template<typename C>
void foo(C&& obj)
{
    bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}

Також зауважте, що зазвичай ви не вдаєтеся до рішення на основі шаблонів, якщо у вас немає вагомих причин, оскільки це робить ваш код важчим для читання. Зазвичай вам слід зосередитись на чіткості та простоті .

Вищенаведені - це просто прості вказівки, але більшу частину часу вони спрямовуватимуть вас на гарні дизайнерські рішення.


Щодо решти власного пошти:

Якщо я перепишу його як [...], буде два ходи, а копія не буде.

Це неправильно. Для початку посилання на 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 не може пов'язуватися з lvalues. Більше того, при прив’язуванні посилання на об'єкт не рухається : це просто прив'язка посилання. Таким чином, буде лише один хід.


Отже, виходячи з вказівок, наведених у першій частині цієї відповіді, якщо ви переймаєтесь кількістю ходів, що створюються, коли ви приймаєте CreditCardзначення, ви можете визначити два перевантаження конструктора, один з яких посилається на значення " const(" CreditCard const&) і "один" посилання на оцінку ( 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&випадок, якщо ви передаєте значення, і завдяки правилам згортання посилань це призведе до активізації цієї функції:

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

Це призведе до копії-конструкцію з creditCard, як ви хочете. З іншого боку, коли буде передано оцінку, 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, це працює навіть частіше, ніж можна було б подумати. b) Будь ласка, зауважте, що в останньому прикладі std::forwardможна викликати лише один раз . Я бачив, як люди ставлять його в петлі і т. Д. І оскільки цю відповідь побачать багато початківців, у IMHO повинно бути жирне позначення "Увага!" - щоб допомогти їм уникнути цієї пастки.
Даніель Фрей

1
@SteveJessop: Крім того, існують технічні проблеми (не обов'язково проблеми) з функціями переадресації ( особливо конструктори, які беруть один аргумент ), здебільшого, це те, що вони приймають аргументи будь-якого типу і можуть перемогти std::is_constructible<>рису типу, якщо вони належним чином SFINAE- обмежене - що для деяких може не бути тривіальним.
Енді Проул

11

Спочатку дозвольте виправити деякі деталі. Коли ви говорите наступне:

буде 2 ходи і жодна копія.

Це помилково. Прив’язка до рецензуючої оцінки не є рухом. Є лише один хід.

Крім того, оскільки 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 ах, правда. Це справді дратує, і я не думаю, що існує легкий масштабований спосіб вирішення.
Р. Мартіньо Фернандес

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 & для всього іншого." Ні. Просто ні.
Взуття

Можливо, додамо, якщо ви хочете змінити значення за допомогою & (без const). Ще я не бачу ... простота досить хороша. Але якщо ви так сказали ...
Девід Флюрі

Деколи потрібно змінювати за допомогою примітивних примітивних посилань, а іноді потрібно зробити копію об'єктів, а іноді доводиться переміщувати об'єкти. Ви не можете просто звести все до POD> за значенням та UDT> за посиланням.
Взуття

Гаразд, це саме те, що я додав після. Може бути надто швидкою відповіддю.
Девід Флері

1

Мені якось незрозуміло найголовніше: проходження параметрів.

  • Якщо ви хочете змінити змінну, передану всередині функції / методу
    • Ви передаєте це за посиланням
    • передаєш його як покажчик (*)
  • Якщо ви хочете прочитати значення / змінну, передану всередині функції / методу
    • ви передаєте це за посиланням const
  • Якщо ви хочете змінити значення, передане всередині функції / методу
    • ви передаєте його нормально , копіюючи об'єкт (**)

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

(**) "нормально" означає конструктор копіювання (якщо ви передаєте об'єкт одного типу параметра) або звичайний конструктор (якщо ви передаєте сумісний тип для класу). Якщо ви передаєте об'єкт, як myMethod(std::string), наприклад, конструктор копій буде використаний, якщо йому std::stringпередано ан , тому вам потрібно переконатися, що такий існує.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.