Как правильно передать параметры?
Я новичок в 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++, потому что я действительно нахожу это, скажем так, не тривиальным. Как бы вы справились с моими примерами, представленными выше?
5 ответов:
САМЫЙ ВАЖНЫЙ ВОПРОС ПЕРВЫЙ:
есть ли какие-либо рекомендации по отправке параметров в C++, потому что я действительно нахожу это, скажем так, не тривиальным
если ваша функция должна изменить исходный объект передается, так что после возврата вызова, изменения этого объекта будут видны вызывающему, то вы должны пройти мимо lvalue ссылка:
void foo(my_class& obj) { // Modify obj here... }если функция не нужно изменять исходный объект, и не нужно создавать его копию (другими словами, ему нужно только наблюдать его состояние), тогда вы должны пройти мимо lvalue ссылка на
const:void foo(my_class const& obj) { // Observe obj here }это позволит вам, чтобы вызвать функцию с значениями lvalue (значения lvalue-это объекты с устойчивой идентичности) и с правосторонние значения (значений 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... }вызов приведенной выше функции всегда будут в одном экземпляре, при передаче значений lvalue, и в движении при прохождении значений rvalue. Если ваша функция должна хранить этот объект где-то, вы можете выполнить дополнительный движение от него (например, в случае
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'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по значению можно определить две перегрузки конструктора, одна из которых принимает ссылку 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)) { }это copy-construction на
creditCard, как вам хотелось бы. С другой стороны, когда rvalue передается,Cбудет выведено, чтобы бытьCreditCard, и эта функция будет инстанцирован:Account(std::string number, float amount, CreditCard&& creditCard) : number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard)) { }это move-construction на
creditCard, который является то, что вы хотите (потому что передаваемое значение является rvalue, и что значит мы уполномочены переехать из него).
во-первых, позвольте мне исправить некоторые детали. Когда вы говорите следующее:
там будет 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)) {}
прежде всего,
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используется по ошибке. Посмотри вверх универсальный ссылок.
Я использую довольно простое правило для общего случая: Используйте копирование для 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; };точность указателя : я почти никогда не использовал их. Единственное преимущество и заключается в том, что они могут быть null, или повторно назначены.
Это немного непонятно для меня самое главное: передача параметров.
- если вы хотите изменить переменная внутри функции / метода
- проходите по ссылке
- вы передаете его как указатель ( * )
- если вы хотите прочитать значение / переменная внутри функции / метода
- вы передаете его по ссылке const
- если вы хотите чтобы изменить значение внутри функции / метода
- вы передаете его обычно путем копирования объекта (**)
(*)указатели могут ссылаться на динамически выделенную память, поэтому, когда это возможно, вы должны предпочесть ссылки указателям, даже если ссылки, в конце концов, обычно реализуются как указатели.
(**) "обычно" означает конструктор копирования (если вы передаете объект тот же тип параметра) или обычным конструктором (если вы передаете совместимый тип для класса). Когда вы передаете объект как
myMethod(std::string), например, конструктор копирования будет использоваться, еслиstd::stringпередается ему, поэтому вы должны убедиться, что он существует.
Comments