コピーコンストラクター以来
MyClass(const MyClass&);
および=演算子のオーバーロード
MyClass& operator = (const MyClass&);
ほぼ同じコード、同じパラメーターを持ち、戻り値が異なるだけですが、両方が使用する共通の関数を持つことは可能ですか?
コピーコンストラクター以来
MyClass(const MyClass&);
および=演算子のオーバーロード
MyClass& operator = (const MyClass&);
ほぼ同じコード、同じパラメーターを持ち、戻り値が異なるだけですが、両方が使用する共通の関数を持つことは可能ですか?
回答:
はい。2つの一般的なオプションがあります。1つ(一般的には推奨されません)はoperator=
、コピーコンストラクターからを明示的に呼び出すことです。
MyClass(const MyClass& other)
{
operator=(other);
}
しかし、operator=
古い状態や自己割り当てから生じる問題に対処することになると、良いものを提供することは挑戦です。また、すべてのメンバーとベースは、から割り当てられる場合でも、最初にデフォルトで初期化されます。other
ます。これは、すべてのメンバーとベースに有効であるとは限らず、有効な場合でも、意味的に冗長であり、実質的にコストがかかる可能性があります。
ますます人気のあるソリューションはoperator=
、コピーコンストラクターとスワップメソッドを使用して実装することです。
MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
swap(tmp);
return *this;
}
あるいは:
MyClass& operator=(MyClass other)
{
swap(other);
return *this;
}
A swap
それだけで内部の所有権を交換し、既存の状態をクリーンアップしたり、新しいリソースを割り当てる必要がないよう関数は、書き込みに一般的に簡単です。
コピーとスワップのイディオムの利点は、自動的に自己割り当てが安全であり、スワップ操作がスローされないことを条件として、例外安全性も非常に高いことです。
例外を完全に安全にするために、「手書き」の割り当て演算子は通常、譲受人の古いリソースの割り当てを解除する前に新しいリソースのコピーを割り当てる必要があります。これにより、新しいリソースの割り当てで例外が発生した場合でも、古い状態を元の状態に戻すことができます。 。これはすべて、コピーアンドスワップで無料で提供されますが、通常はより複雑であるため、最初から行うとエラーが発生しやすくなります。
注意すべきことの1つは、swapメソッドが真のスワップでありstd::swap
、コピーコンストラクターと代入演算子自体を使用するデフォルトではないことを確認することです。
通常、メンバーごとswap
に使用されます。std::swap
動作し、すべての基本タイプとポインタタイプで「スローなし」が保証されます。ほとんどのスマートポインタは、スローなしの保証と交換することもできます。
operator=
、copy ctorからのすべては、実際にはかなり悪いです。なぜなら、最初にすべての値をデフォルトに初期化して、直後に他のオブジェクトの値でオーバーライドするからです。
assign
場合によっては、コピーctorと代入演算子の両方でメンバー関数を使用するのが合理的だと思います(軽量クラスの場合)。その他の場合(リソースを大量に消費する/使用する場合、ハンドル/本体)、もちろんコピー/スワップが最適です。
コピーコンストラクタは、以前はrawメモリであったオブジェクトの初回初期化を実行します。代入演算子OTOHは、既存の値を新しい値でオーバーライドします。多くの場合、これには古いリソース(メモリなど)の破棄と新しいリソースの割り当てが含まれます。
2つの間に類似性がある場合、それは代入演算子が破棄とコピー構築を実行することです。一部の開発者は、インプレース破棄とそれに続くプレースメントコピー構築によって実際に割り当てを実装していました。ただし、これは非常に悪い考えです。(これが、派生クラスの代入中に呼び出された基本クラスの代入演算子である場合はどうなりますか?)
swap
チャールズが示唆したように、今日では通常、標準的なイディオムと見なされているものが使用されています。
MyClass& operator=(MyClass other)
{
swap(other);
return *this;
}
これはcopy-construction(other
コピーされることに注意)とdestroy(関数の最後で破棄される)を使用します-そしてそれも正しい順序で使用します:破棄の前に構築(失敗するかもしれません)(失敗してはいけません)。
何か気になること:
MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
swap(tmp);
return *this;
}
まず、私の心が「コピー」を考えているときに「スワップ」という言葉を読むと、私の常識が刺激されます。また、私はこの派手なトリックの目的に疑問を投げかけています。はい、新しい(コピーされた)リソースを構築する際の例外は、スワップの前に発生する必要があります。これは、新しいデータがすべてライブになる前に入力されていることを確認する安全な方法のようです。
それはいいです。では、スワップ後に発生する例外についてはどうでしょうか。(一時オブジェクトがスコープ外になったときに古いリソースが破棄された場合)割り当てのユーザーの観点からは、操作は失敗しましたが、失敗しませんでした。これには大きな副作用があります。コピーは実際に発生しました。失敗したのは、一部のリソースのクリーンアップだけでした。外部からの操作が失敗したように見えても、宛先オブジェクトの状態が変更されています。
したがって、「スワップ」の代わりに、より自然な「転送」を行うことを提案します。
MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
transfer(tmp);
return *this;
}
一時オブジェクトの構築はまだありますが、次の即時アクションは、ソースのリソースをそこに移動する前に(そして、それらが二重に解放されないようにNULLにする)、宛先の現在のすべてのリソースを解放することです。
{construct、move、destruct}の代わりに、{construct、destruct、move}を提案します。最も危険な行動であるこの動きは、他のすべてが解決した後に最後に行われる行動です。
はい、どちらのスキームでも破壊の失敗は問題です。データが破損している(思っていなかったときにコピーされた)か、失われた(思っていなかったときに解放された)。紛失は破損よりも優れています。悪いデータより良いデータはありません。
スワップの代わりに転送します。それはとにかく私の提案です。
First, reading the word "swap" when my mind is thinking "copy" irritates
->ライブラリライターとして、あなたは通常、一般的な方法(コピー+スワップ)を知っており、核心はmy mind
です。あなたの心は実際にはパブリックインターフェイスの後ろに隠されています。それが再利用可能なコードのすべてです。