条項23:オブジェクトを返す必要がある場合は、リファレンスを返さないでください.

7352 ワード

アインシュタインは、できるだけ簡単にできるが、あまり簡単ではないと提案したという.C++言語で似たような言い方は、できるだけプログラムを効率的にしますが、あまり効率的ではありません.
プログラマが効率的な「伝達値」のハンドルを握ると(条項22参照)、プログラムに隠された伝達値操作を掘り出すことを憎む極端になります.彼らがたゆまず純粋な「伝引用」を追求する過程で、彼らはもう一つの深刻な誤りを犯すことが避けられないだろう.存在しない対象の引用を伝える.これはいいことではない.
2つの有理数を乗算するための友元関数を含む有理数を表すクラスを見てください.
class rational {

public:

	rational(int numerator = 0, int  denominator = 1);



	...



private:

	int n, d;						//      



friend

	const rational						//     21:   

	operator*(const rational& lhs,				//      const

			const rational& rhs)

};



inline const rational operator*(const rational& lhs,

			const rational& rhs)

{

	return rational(lhs.n * rhs.n, lhs.d * rhs.d);

}


明らかに、このバージョンのoperator*は、オブジェクトの構造とプロファイルのオーバーヘッドを考慮しなければ、プログラマーとしての責任を逃れるために、オブジェクトの結果を値で返します.もう1つの明らかな事実は、確かに必要でない限り、誰もこのような一時的なオブジェクトのオーバーヘッドを負担したくないということです.では、問題は、確かに必要ですか?
答えは、引用を返すことができれば、もちろん必要ありません.ただし、参照は名前であり、他の既存のオブジェクトの名前であることを覚えておいてください.いつ引用された声明を見ても、すぐに自分に「もう一つの名前は何ですか」と聞かなければなりません.必ず別の名前があるからです(条項M 1参照).operator*では、関数が参照を返す場合は、2つのオブジェクトを乗算した結果を含む他の既存のrationalオブジェクトの参照を返さなければなりません.
ただし、operator*を呼び出す前にこのようなオブジェクトが存在することを望むのは無理です.つまり、次のコードがあれば、
rational a(1, 2);				// a = 1/2

rational b(3, 5);				// b = 3/5

rational c = a * b;				// c   3/10


3/10の値が既に存在することが望ましい有理数は現実的ではない.operator*が必ずこのような数の参照を返す場合は、この数のオブジェクトを自分で作成する必要があります.
1つの関数には、スタック内またはスタック上の2つの方法で新しいオブジェクトを作成するしかありません.スタックにオブジェクトを作成するときにローカル変数の定義が伴います.この方法でoperator*を書きます.
//             

inline const rational& operator*(const rational& lhs,

					const rational& rhs)

{

	rational result(lhs.n * rhs.n, lhs.d *  rhs.d);

	return result;

}


この方法は、コンストラクション関数が呼び出されるのを避けることを目標としているが、resultは他のオブジェクトのように構築されなければならないため、否決されるべきである.さらに,この関数には,局所オブジェクトの参照を返すもう一つの深刻な問題があり,この誤りについて条項31で深く議論した.
では、スタックにオブジェクトを作成してリファレンスを返しますか?スタックベースのオブジェクトはnewを使用して生成されるのでoperator*と書くべきです.
//             

inline const rational& operator*(const rational&  lhs,

					const rational& rhs)

{

	rational *result =

		new rational(lhs.n * rhs.n, lhs.d * rhs.d);

	return *result;

}


まず、newが割り当てたメモリは、適切なコンストラクション関数を呼び出すことによって初期化されるため、コンストラクション関数呼び出しのオーバーヘッドを負担する必要があります(条項5およびM8を参照).また、deleteでnew生成されたオブジェクトを削除する責任は誰ですか?
実際には、これは絶対にメモリの漏洩です.operator*の呼び出し元を説得して関数の戻り値アドレスを取りに行ってdeleteで削除することはできますが(絶対に不可能です.条項31はこのようなコードがどのようなものかを示しています)、いくつかの複雑な式は名前のない一時値を生成し、プログラマーは得られません.例:
rational w, x, y, z;

w = x * y * z;


2つのoperator*の呼び出しは、名前のない一時値を生成し、プログラマが表示できないため削除できません.(条項31参照)
普通の熊や普通のプログラマーより頭がいいと思うかもしれません.スタックとスタックにオブジェクトを作成する方法では、コンストラクション関数の呼び出しが避けられないことに気づいたかもしれません.もしかすると、私たちの最初の目標は、このような構造関数の呼び出しを避けるためだったことを思い出したかもしれません.もしかすると、1つの構造関数だけですべてを見ることができる方法があるかもしれません.operator*は、関数の内部で定義された静的rationalオブジェクトの参照を返します.
//             

inline const rational& operator*(const rational&  lhs,

					const rational& rhs)

{

	static rational  result;		//           

						//     

	lhs rhs   ,    result;

	return result;

}


この方法は芝居のように見えますが、実際に上記の擬似コードを実装すると、rational構造関数を呼び出さないとresultの正確な値を与えることはできませんが、このような呼び出しを避けることは私たちが議論するテーマです.上の偽コードを実現しても、どんなに賢くても、この不幸な設計を最終的に救うことはできません.
なぜか知りたいのは、次の合理的なユーザーコードを見てみましょう.
bool operator==(const rational& lhs,		//  rationals operator==

			const rational& rhs);

rational a, b, c, d;

...

if ((a * b) == (c * d)) {

	       ;

}

else {

	        ;

}


見えますか?((a*b)=(c*d))は永遠にtrueであり、a,b,c,dがどんな値であっても!
上記の等しい判断文を等価な関数形式で書き換えると、この悪行の原因が分かりやすくなります.
if (operator==(operator*(a, b), operator*(c, d)))

注意operator==が呼び出されると、常に2つのoperator*が呼び出され、各呼び出しはoperator*内部の静的rationalオブジェクトの参照を返します.そこで、上記の文は実際にoperator==に「operator*内部の静的rationalオブジェクトの値」と「operator*内部の静的rationalオブジェクトの値」を比較するように要求していますが、このような比較が等しくないのはおかしいですね.
幸いなことに、私の以上の説明はあなたを説得するのに十分です:“operator*のような関数の中で1つの引用を返したいです”は実際には時間を浪費しています.しかし、私は幸運がいつも自分に来ると信じているほど幼稚ではありません.一部の人は--これらの人が誰を指しているか知っています--今考えています.「うん、上の方法は、静的変数が足りなければ、静的配列を使うことができるかもしれません......」
これで止めてください!私たちはまだうんざりしていませんか?
私は自分にサンプルコードを書くことができません.この設計は、上記のような考えを持っていても恥ずかしいからです.まず、nを選択して配列のサイズを指定する必要があります.nが小さすぎると,関数の戻り値を格納する場所がなく,我々が前に否定した「単一静的変数を用いた設計」に比べて改善されない.nが大きすぎると、関数が最初に呼び出されたときに配列内の各オブジェクトが作成されるため、プログラムのパフォーマンスが低下します.これにより、n個のコンストラクション関数とn個のコンストラクション関数のオーバーヘッドが発生し、この関数が一度だけ呼び出されても発生します.「optimization」(最適化)がソフトウェアのパフォーマンスを向上させるプロセスを指すとすれば、今では「pessimization」(最悪化)と呼ぶことができる.最後に、必要な値を配列のオブジェクトにどのように配置し、どのくらいのオーバーヘッドが必要かを考えてみましょう.オブジェクト間で値を伝達する最も直接的な方法は、値を付与することですが、値を付与するコストはどのくらいですか?一般的には、構造関数を呼び出す(古い値を破壊する)ことに加え、構造関数を呼び出す(新しい値をコピーする)ことに相当します.しかし、私たちの今の目標は構造と分析のオーバーヘッドを避けるためです.現実に直面しましょう:この方法も絶対に選択できません.
したがって、新しいオブジェクトを返さなければならない関数を書く正しい方法は、この関数を新しいオブジェクトに返すことです.rationalのoperator*では、次のコード(最初に見たコード)か、本質的に等価なコードかを意味します.
inline const rational operator*(const rational& lhs,

					const rational& rhs)

{

	return rational(lhs.n * rhs.n, lhs.d *  rhs.d);

}


確かに、これは「operator*の戻り値の構造と解析に伴うオーバーヘッド」を引き起こすが、結局は小さな代価で正しいプログラムの実行行為を交換するだけである.さらに、すべてのプログラム設計言語と同様に、C++はコンパイラの設計者が生成されたコードの性能を向上させるためにいくつかの最適化措置を採用することを可能にするため、operator*の戻り値が安全に除去される場合がある(条項M 20参照).コンパイラがこのような最適化を採用した場合(現在のほとんどのコンパイラがそうしている)、プログラムは以前と同じように動作し続け、予想よりも実行速度が速いにすぎません.
以上の議論は、戻り参照と戻りオブジェクトの間で決定する必要がある場合、正しい機能を果たすことができるものを選択することに帰結します.どのようにこの選択によって生じた代価をできるだけ小さくするかについては、コンパイラのメーカーが考えていることです.