1. ホーム
  2. c++

[解決済み] パラメータを正しく渡すには?

2022-10-11 03:45:43

質問

私は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);

このシナリオでは、明らかにクレジットカードが2回コピーされます。 このコンストラクタを次のように書き換えると

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

は1つのコピーになります。 もし、これを

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

移動は2回で、コピーはありません。

あるパラメータをコピーしたい場合もあれば、そのオブジェクトを作成するときにコピーしたくない場合もあるかと思います。

私はC#から来たので、参照に慣れている私には少し奇妙で、各パラメータに2つのオーバーロードがあるはずだと思いますが、私は間違っていることを知っています。

C++でパラメータを送信する方法についてのベストプラクティスはありますか?あなたは、上記の私の例をどのように扱うでしょうか?

どのように解決するのですか?

最も重要な質問です。

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を渡した場合は常に1つのコピーになり、rvaluesを渡した場合は1つの移動になります。もし、あなたの関数がこのオブジェクトをどこかに保存する必要があるならば、追加で 移動 を実行することができます (例えば foo() の値をデータメンバに格納する必要があるメンバ関数です。 ).

ムーブが高価な場合 型のオブジェクトに対して my_class 型のオブジェクトに対して移動が高価である場合、 オーバーロードを考慮することができます。 foo() をオーバーロードし、lvalueのための一つのバージョンを提供する(lvalueの参照を受け入れ const ) と、rvalues (rvalueの参照を受け取る) のための1つのバージョンです。

// 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...
}

このデザインについてもっと知りたい方は Scott Meyersによるこの講演は (ただし、この用語の " ユニバーサル リファレンス という用語は非標準であることに注意してください)。

注意すべき点として std::forward は通常、最終的に を動かす になってしまうので、同じオブジェクトを何度も転送することは、一見何の問題もないように見えても、トラブルの元となる可能性があります。例えば、同じオブジェクトから2回移動する!などです。ですから、これをループに入れたり、関数呼び出しの中で同じ引数を何度も転送しないように注意してください。

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

また、コードが読みにくくなるため、十分な理由がない限りテンプレートベースの解決策に頼らないのが普通であることにも注意してください。 通常、明確さとシンプルさに焦点を当てるべきです。 .

上記は簡単なガイドラインに過ぎませんが、ほとんどの場合、良いデザインの決定へと導いてくれるでしょう。


あなたの投稿の続きについて。

もし、[...]のように書き換えた場合、2手となり、コピーはできません。

これは正しくありません。そもそもrvalueの参照はlvalueにバインドできないので、この場合、型 CreditCard 型の rvalue をコンストラクタに渡した場合のみコンパイルされます。例えば

// 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にバインドすることができないからです。さらに オブジェクトへの参照をバインドする場合、移動は行われません。 : それは単なる参照バインドです。従って 1 の動きしかありません。


この回答の最初の部分で提供されたガイドラインに基づき、もしあなたが CreditCard への lvalue 参照を取る 2 つのコンストラクタ オーバーロードを定義することができます。 const ( CreditCard const& ) と、r値の参照を取るもの ( CreditCard&& ).

オーバーロードの解決は、l値を渡す場合は前者(この場合、1回のコピーが実行されます)を、r値を渡す場合は後者(この場合、1回の移動が実行されます)を選択します。

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 のような、希望するようなものを作ることができる。一方、rvalueが渡された場合。 C は次のように推論される。 CreditCard であると推論され、この関数が代わりにインスタンス化されます。

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

これによって ムーブコンストラクション creditCard これはあなたが望むものです(渡される値はrvalueであり、それは私たちがそこから移動することを許可されていることを意味するからです)。