TL; DR:C ++では、const参照による受け渡しは今でも良い考えです。時期尚早な最適化ではありません。
TL; DR2:ほとんどの格言は、意味があるまで意味をなさない。
目的
この回答は、C ++コアガイドライン(amonのコメントで最初に言及された)のリンクされた項目を少し拡張しようとするものです。
この答えは、プログラマーのサークル内で広く流通しているさまざまな格言、特に矛盾する結論や証拠の間の和解の問題を適切に考えて適用する方法の問題に対処しようとはしていません。
適用性
この回答は、関数呼び出し(同じスレッド上の分離できないネストされたスコープ)のみに適用されます。
(補足)通行可能なものがスコープをエスケープできる場合(つまり、外側のスコープを超える可能性のあるライフタイムがある場合)、オブジェクトのライフタイム管理に対するアプリケーションのニーズを他よりも早く満たすことが重要になります。通常、これには、スマートポインターなどのライフタイム管理も可能な参照を使用する必要があります。別の方法として、マネージャーを使用することもできます。ラムダは一種の取り外し可能なスコープであることに注意してください。ラムダキャプチャは、オブジェクトスコープを持つように動作します。したがって、ラムダキャプチャに注意してください。また、ラムダ自体が渡される方法に注意してください-コピーまたは参照によって。
値渡しする場合
ある値に対してスカラーれる通信ごとに可変性(共有参照)を必要としない(マシン・レジスタ内に収まる値セマンティックを有する標準プリミティブ)、値渡し。
呼び出し先がオブジェクトまたは集計の複製を必要とする状況では、値で渡します。この場合、呼び出し先のコピーは複製されたオブジェクトのニーズを満たします。
参照渡しする場合など
他のすべての状況では、ポインター、参照、スマートポインター、ハンドル(ハンドル:ボディイディオムを参照)などで渡します。このアドバイスに従う場合は、通常どおりconst-correctnessの原則を適用してください。
メモリフットプリントが十分に大きいもの(集合体、オブジェクト、配列、データ構造)は、パフォーマンス上の理由から、常に参照渡しを容易にするように設計する必要があります。このアドバイスは、数百バイト以上の場合に確実に適用されます。数十バイトの場合、このアドバイスは境界線です。
異常なパラダイム
意図的にコピーが多い特殊なプログラミングパラダイムがあります。たとえば、文字列処理、シリアル化、ネットワーク通信、分離、サードパーティライブラリのラッピング、共有メモリプロセス間通信など。これらのアプリケーション領域またはプログラミングパラダイムでは、データは構造体から構造体にコピーされ、場合によっては再パッケージ化されます。バイト配列。
最適化が検討される前に、言語仕様がこの回答にどのように影響するか。
Sub-TL; DR参照の伝播では、コードを呼び出さないでください。const-referenceを渡すと、この基準が満たされます。ただし、他のすべての言語はこの基準を簡単に満たします。
(初心者のC ++プログラマは、このセクションを完全にスキップすることをお勧めします。)
(このセクションの冒頭はgnasher729の回答に一部影響を受けています。ただし、別の結論に達しました。)
C ++では、ユーザー定義のコピーコンストラクターと代入演算子を使用できます。
(これは(かつて)大胆な選択でした(驚くべきことでしたし、後悔していました。これは間違いなく、言語設計における今日の許容範囲からの逸脱です。)
C ++プログラマーが定義していない場合でも、C ++コンパイラーは言語原則に基づいてそのようなメソッドを生成し、以外の追加コードを実行する必要があるかどうかを判断する必要がありますmemcpy
。たとえば、メンバーを含むclass
/ には、コピーコンストラクターと非自明な代入演算子が必要です。struct
std::vector
他の言語では、オブジェクトが言語設計により参照セマンティクスを持っているため、コピーコンストラクターとオブジェクトのクローン作成は推奨されません(アプリケーションのセマンティクスにとって絶対に必要な場合や意味がある場合を除く)。これらの言語には通常、スコープベースの所有権や参照カウントではなく、到達可能性に基づいたガベージコレクションメカニズムがあります。
参照またはポインター(const参照を含む)がC ++(またはC)で渡されると、プログラマーはアドレス値の伝搬以外の特別なコード(ユーザー定義またはコンパイラー生成関数)が実行されないことを保証されます。 (参照またはポインター)。これは、C ++プログラマーが慣れている動作の明確さです。
しかし、背景は、C ++言語が不必要に複雑であるため、このような明確な動作は、核フォールアウトゾーン周辺のオアシス(生存可能な生息地)のようなものです。
祝福(またはin辱)を追加するために、C ++では、ユーザー定義の移動演算子(move-constructorsおよびmove-assignment演算子)のパフォーマンスを向上させるために、ユニバーサルリファレンス(r-values)を導入しています。これは、コピーとディープクローニングの必要性を減らすことにより、関連性の高いユースケース(1つのインスタンスから別のインスタンスへのオブジェクトの移動(転送))に役立ちます。ただし、他の言語では、このようなオブジェクトの移動について話すのは非論理的です。
(トピック外のセクション)「スピードを望みますか?値渡し!」という記事専用のセクション。2009年頃に書かれました。
この記事は2009年に作成され、C ++でのr値の設計上の正当化について説明しています。その記事は、前のセクションでの私の結論に対する有効な反論を提示します。ただし、この記事のコード例とパフォーマンスの主張は長い間反論されてきました。
Sub-TL; DR C ++のr値セマンティクスの設計によりSort
、たとえば、関数で驚くほどエレガントなユーザー側のセマンティクスが可能になります。このエレガントさは、他の言語でモデル化(模倣)することは不可能です。
ソート関数は、データ構造全体に適用されます。前述のように、大量のコピーが含まれる場合は時間がかかります。パフォーマンスの最適化(実際に関連する)として、ソート関数は、C ++以外の非常に多くの言語で破壊的になるように設計されています。破壊的とは、ソートの目標を達成するためにターゲットデータ構造が変更されることを意味します。
C ++では、ユーザーは、2つの実装のいずれかを呼び出すことができます。パフォーマンスが向上した破壊的な実装、または入力を変更しない通常の実装です。(簡潔にするためにテンプレートは省略されています。)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
並べ替えのほかに、この優雅さは、再帰的な分割によって、配列内の破壊的中央値検出アルゴリズム(最初は並べ替えられていない)の実装にも役立ちます。
ただし、ほとんどの言語では、配列に破壊的なソートアルゴリズムを適用する代わりに、バランスの取れたバイナリ検索ツリーアプローチをソートに適用します。したがって、この手法の実用的な関連性は、見かけほど高くありません。
コンパイラの最適化がこの回答に与える影響
インライン化(およびプログラム全体の最適化/リンク時の最適化)がいくつかのレベルの関数呼び出しに適用されると、コンパイラーは(場合によっては徹底的に)データの流れを見ることができます。これが発生すると、コンパイラは多くの最適化を適用できます。その一部はメモリ内のオブジェクト全体の作成を排除できます。通常、この状況が当てはまる場合、コンパイラーは徹底的に分析できるため、パラメーターが値で渡されるかconst-referenceで渡されるかは関係ありません。
ただし、下位レベルの関数が分析を超えたもの(たとえば、コンパイル外の別のライブラリにあるもの、複雑すぎる呼び出しグラフなど)を呼び出す場合、コンパイラーは防御的に最適化する必要があります。
マシンのレジスタ値よりも大きいオブジェクトは、明示的なメモリロード/ストア命令、または由緒あるmemcpy
関数の呼び出しによってコピーされる場合があります。一部のプラットフォームでは、コンパイラーは2つのメモリー位置間を移動するためにSIMD命令を生成し、各命令は数十バイト(16または32)移動します。
冗長性または視覚的な混乱の問題に関する議論
C ++プログラマーはこれに慣れています。つまり、プログラマーがC ++を嫌わない限り、ソースコード内のconst参照の書き込みまたは読み取りのオーバーヘッドは恐ろしくありません。
費用便益分析は以前に何度も行われた可能性があります。引用すべき科学的なものがあるかどうかはわかりません。ほとんどの分析は非科学的または再現性がないと思います。
ここに私が想像するものがあります(証明や信頼できる参照なし)...
- はい、この言語で書かれたソフトウェアのパフォーマンスに影響します。
- コンパイラーがコードの目的を理解できれば、それを潜在的に自動化するのに十分に賢い可能性があります。
- 残念ながら、(機能的な純度ではなく)可変性を好む言語では、コンパイラはほとんどのものを変異していると分類するため、constnessの自動推論はほとんどのものを非constとして拒否します
- 精神的なオーバーヘッドは人によって異なります。これが大きな頭痛の種だと思う人は、C ++を実行可能なプログラミング言語として拒否したでしょう。