Herb Sutterの本Exceptional C++(1999)で、彼は項目10の解決策に言葉を持っています:
「例外安全ではない」と「貧弱なデザイン」は密接に関連しています。コードの一部が例外に対して安全でない場合、それは通常問題なく、簡単に修正できます。ただし、コードの基礎となる設計のためにコードの一部を例外セーフにできない場合、それはほとんどの場合、設計が不適切であることを示しています。
例1:2つの異なる責任を持つ関数を例外セーフにすることは困難です。
例2:必要な方法で記述されたコピー代入演算子自己代入をチェックも、おそらく例外的に安全ではありません
「自己割り当てのチェック」という用語の意味を教えてください。
[問い合わせ]
DaveとAndreyTは、「自己割り当てのチェック」の意味を正確に示しています。それは良い。しかし、問題はまだ終わっていません。 「自己割り当てのチェック」が「例外の安全性」を損なうのはなぜですか(Hurb Sutterによると)?呼び出し元が自己割り当てを行おうとすると、その「チェック」は割り当てが発生しないかのように機能します。本当に痛いですか?
[メモ1] Herbの本の後半の項目38Object Identityで、彼は自己割り当てについて説明しています。
この場合のより重要な質問は、「自己割り当てをチェックする必要があるように書かれている」という意味です。
つまり、適切に設計された代入演算子は、自己代入をチェックするためにneedを実行すべきではありません。自分自身へのオブジェクトの割り当ては、自己割り当ての明示的なチェックを実行しなくても、正しく機能します(つまり、「何もしない」という最終的な効果があります)。
たとえば、次のような単純な配列クラスを実装したい場合
class array {
...
int *data;
size_t n;
};
そして、代入演算子の次の実装を思いつきました
array &array::operator =(const array &rhs)
{
delete[] data;
n = rhs.n;
data = new int[n];
std::copy_n(rhs.data, n, data);
return *this;
}
自己割り当ての場合は明らかに失敗するため、その実装は「悪い」と見なされます。
それを「修正」するために、明示的な自己割り当てチェックを追加することができます
array &array::operator =(const array &rhs)
{
if (&rhs != this)
{
delete[] data;
n = rhs.n;
data = new int[n];
std::copy_n(rhs.data, n, data);
}
return *this;
}
または「チェックレス」アプローチに従う
array &array::operator =(const array &rhs)
{
size_t new_n = rhs.n;
int *new_data = new int[new_n];
std::copy_n(rhs.data, new_n, new_data);
delete[] data;
n = new_n;
data = new_data;
return *this;
}
後者のアプローチは、explicitチェックを行わなくても、自己割り当て状況で正しく機能するという意味で優れています。 (この実装は、例外の安全性の観点からはまだ完璧とは言えません。ここでは、自己割り当てを処理するための「チェック」アプローチと「チェックレス」アプローチの違いを説明します)。後のチェックレス実装は、よく知られているコピーアンドスワップイディオムを使用してよりエレガントに記述できます。
これは、自己割り当ての明示的なチェックを回避する必要があるという意味ではありません。このようなチェックはパフォーマンスの観点からは理にかなっています。結局のところ、「何もしない」ことになるだけの長い一連の操作を実行しても意味がありません。しかし、適切に設計された代入演算子では、このようなチェックは正確さの観点からは必要ありません。
C++コアガイドから
Foo& Foo::operator=(const Foo& a) // OK, but there is a cost
{
if (this == &a) return *this;
s = a.s;
i = a.i;
return *this;
}
これは明らかに安全で、明らかに効率的です。ただし、100万回の割り当てごとに1つの自己割り当てを行うとしたら?これは約100万回の冗長なテストです(ただし、答えは本質的に常に同じであるため、コンピューターの分岐予測子は本質的に毎回正しく推測します)。考慮してください:
Foo& Foo::operator=(const Foo& a) // simpler, and probably much better
{
s = a.s;
i = a.i;
return *this;
}
注:上記のコードは、ポインターのないクラスにのみ適用されます。ポインターを持つクラスは動的メモリを指します。 Antの回答を参照してください。
MyClass& MyClass::operator=(const MyClass& other) // copy assignment operator
{
if(this != &other) // <-- self assignment check
{
// copy some stuff
}
return *this;
}
オブジェクト自体への割り当ては誤りですが、論理的にクラスインスタンスが変更されることはありません。自分自身への割り当てがそれを変更するクラスを設計することに成功した場合、それは不十分に設計されています。
自己割り当てを確認する一般的な理由は、新しいデータをコピーする前に自分のデータを破棄するためです。この代入演算子の構造も、例外的に安全ではありません。
補足として、比較は毎回実行する必要がありますが、自己割り当ては非常にまれであり、それが発生した場合、これはプログラムの論理エラーです(本当に)ので、自己割り当てはパフォーマンスにまったくメリットがないと判断されました。 。これは、プログラムの過程で、それがサイクルの無駄に過ぎないことを意味します。