C ++コンパイラがクラスのコピーコンストラクタを作成することを知っています。その場合、ユーザー定義のコピーコンストラクターを作成する必要がありますか?いくつか例を挙げていただけますか?
C ++コンパイラがクラスのコピーコンストラクタを作成することを知っています。その場合、ユーザー定義のコピーコンストラクターを作成する必要がありますか?いくつか例を挙げていただけますか?
回答:
コンパイラーによって生成されたコピーコンストラクターは、メンバーごとのコピーを行います。時にはそれだけでは不十分です。例えば:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
この場合、メンバーのメンバーごとのコピーはstored
バッファーを複製しないため(ポインターのみがコピーされます)、バッファーを共有する最初の破棄されたコピーはdelete[]
正常に呼び出され、2番目は未定義の動作に遭遇します。ディープコピーコピーコンストラクター(および代入演算子)が必要です。
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
そうあるべきだと思いますdelete [] stored;
std::string
です。一般的な考え方は、リソースを管理するユーティリティクラスのみがビッグスリーをオーバーロードする必要があり、他のすべてのクラスはそれらのユーティリティクラスを使用するだけで、ビッグスリーを定義する必要がなくなるというものです。
のルールがRule of Five
引用されていないことに少し腹を立てています。
このルールは非常に単純です。
5つの法則:
デストラクタ、コピーコンストラクタ、コピー割り当て演算子、ムーブコンストラクタ、またはムーブ代入演算子のいずれかを作成する場合は、おそらく他の4つを作成する必要があります。
しかし、あなたが従うべきより一般的なガイドラインがあります。それは、例外安全なコードを書く必要性から導き出されます:
各リソースは専用のオブジェクトで管理する必要があります
ここ@sharptooth
のコードはまだ(ほとんど)問題ありませんが、クラスに2番目の属性を追加した場合は問題ありません。次のクラスについて考えてみます。
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
場合はどうなるのnew Bar
スロー?が指すオブジェクトをどのように削除しますmFoo
か?解決策があります(関数レベルのtry / catch ...)、それらは単にスケーリングしません。
この状況に対処する適切な方法は、生のポインターの代わりに適切なクラスを使用することです。
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
同じコンストラクターの実装(または実際にはを使用make_unique
)で、例外安全性を無料で利用できるようになりました!!! ワクワクしませんか?そして何よりも、適切なデストラクタについて心配する必要がなくなりました。これらの操作を定義していないので、私は自分Copy Constructor
で書く必要があります...しかし、ここでは重要ではありません;)Assignment Operator
unique_ptr
したがって、sharptooth
のクラスの再検討:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
私はあなたのことを知りませんが、私は私の方が簡単だと思います;)
私の練習から思い出して、コピーコンストラクターを明示的に宣言/定義する必要がある場合の次のケースを考えることができます。ケースを2つのカテゴリに分類しました
このセクションでは、コピーコンストラクターを宣言/定義することが、そのタイプを使用するプログラムの正しい操作に必要な場合を示します。
このセクションを読んだ後、コンパイラーが独自にコピーコンストラクターを生成できるようにすることのいくつかの落とし穴について学びます。したがって、seandが彼の回答で述べたように、新しいクラスのコピー可能性をオフにし、後で本当に必要なときに意図的に有効にすることは常に安全です。
プライベートコピーコンストラクターを宣言し、その実装を提供しないでください(そのため、そのタイプのオブジェクトがクラス自体のスコープまたはその友人によってコピーされた場合でも、リンク段階でビルドが失敗します)。
=delete
最後にコピーコンストラクタを宣言します。
これは最もよく理解されているケースであり、実際には他の回答で言及されている唯一のケースです。shaprtoothはそれをかなりうまくカバーしています。オブジェクトによって排他的に所有されるべきである深くコピーするリソースは、動的に割り当てられたメモリが1種類にすぎない、あらゆるタイプのリソースに適用できることだけを追加したいと思います。必要に応じて、オブジェクトを深くコピーすることも必要になる場合があります
すべてのオブジェクトが(どのように構築されていても)何らかの方法で登録する必要があるクラスについて考えてみます。いくつかの例:
最も単純な例:現在存在するオブジェクトの総数を維持する。オブジェクトの登録は、静的カウンターをインクリメントすることです。
より複雑な例は、シングルトンレジストリを使用することです。このレジストリには、そのタイプの既存のすべてのオブジェクトへの参照が格納されます(通知をすべてのオブジェクトに配信できるようにするため)。
参照カウントされたスマートポインタは、このカテゴリの特殊なケースと見なすことができます。新しいポインタは、グローバルレジストリではなく、共有リソースに「登録」されます。
このような自己登録操作は、そのタイプの任意のコンストラクターによって実行される必要があり、コピーコンストラクターも例外ではありません。
一部のオブジェクトは、異なるサブオブジェクト間で直接相互参照する重要な内部構造を持っている場合があります(実際、このような内部相互参照は1つだけでこのケースをトリガーできます)。コンパイラが提供するコピーコンストラクタは、内部のオブジェクト内の関連付けを解除し、それらをオブジェクト間の関連付けに変換します。
例:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
オブジェクトは、いくつかの状態(例えば、デフォルトで構築状態)にあるとしながら、コピーしても安全なクラスがあるかもしれないそうでない場合は、コピーしても安全。安全にコピーできるオブジェクトのコピーを許可する場合は、防御的にプログラミングする場合は、ユーザー定義のコピーコンストラクターで実行時チェックを行う必要があります。
コピー可能である必要があるクラスが、コピー不可能なサブオブジェクトを集約する場合があります。通常、これは観察不可能な状態のオブジェクトで発生します(この場合については、以下の「最適化」セクションで詳しく説明します)。コンパイラは、単にそのケースを認識するのに役立ちます。
コピー可能である必要があるクラスは、準コピー可能タイプのサブオブジェクトを集約できます。準コピー可能な型は、厳密な意味でのコピーコンストラクターを提供しませんが、オブジェクトの概念的なコピーを作成できる別のコンストラクターを備えています。型を準コピー可能にする理由は、型のコピーセマンティクスについて完全な合意がない場合です。
たとえば、オブジェクトの自己登録の場合を再検討すると、オブジェクトが完全なスタンドアロンオブジェクトである場合にのみ、オブジェクトをグローバルオブジェクトマネージャに登録する必要がある場合があると主張できます。それが別のオブジェクトのサブオブジェクトである場合、それを管理する責任はそれを含むオブジェクトにあります。
または、浅いコピーと深いコピーの両方をサポートする必要があります(いずれもデフォルトではありません)。
次に、最終的な決定はそのタイプのユーザーに任されます。オブジェクトをコピーするときは、目的のコピー方法を(追加の引数を介して)明示的に指定する必要があります。
プログラミングに対する非防御的アプローチの場合、通常のコピーコンストラクターと準コピーコンストラクターの両方が存在する可能性もあります。これは、ほとんどの場合、単一のコピー方法を適用する必要がある場合に正当化できますが、まれですがよく理解されている状況では、別のコピー方法を使用する必要があります。そうすれば、コンパイラーは、コピーコンストラクターを暗黙的に定義できないと文句を言うことはありません。そのタイプのサブオブジェクトを準コピーコンストラクターを介してコピーする必要があるかどうかを覚えて確認するのは、ユーザーの責任です。
まれに、オブジェクトの観察可能な状態のサブセットが、オブジェクトのIDの不可分の一部を構成する(または見なされる)場合があり、他のオブジェクトに転送できないようにする必要があります(これは多少物議を醸す可能性があります)。
例:
オブジェクトのUID(ただし、IDは自己登録の動作で取得する必要があるため、これも上記の「自己登録」の場合に属します)。
新しいオブジェクトがソースオブジェクトの履歴を継承してはならず、代わりに単一の履歴アイテム「<OTHER_OBJECT_ID>から<TIME>にコピーされた」で開始する必要がある場合のオブジェクトの履歴(たとえば、元に戻す/やり直しスタック)。
このような場合、コピーコンストラクタは対応するサブオブジェクトのコピーをスキップする必要があります。
コンパイラーが提供するコピーコンストラクターの署名は、サブオブジェクトで使用できるコピーコンストラクターによって異なります。少なくとも1つのサブオブジェクトに実際のコピーコンストラクター(定数参照によってソースオブジェクトを取得)がなく、代わりに変更コピーコンストラクター(非定数参照によってソースオブジェクトを取得)がある場合、コンパイラーは選択の余地がありません。ただし、変更するコピーコンストラクタを暗黙的に宣言してから定義します。
では、サブオブジェクトの型の「変更」コピーコンストラクターが実際にソースオブジェクトを変更しない場合(そして、const
キーワードを知らないプログラマーによって作成された場合)はどうなるでしょうか。不足しているを追加してそのコードを修正できない場合const
、他のオプションは、正しい署名を使用して独自のユーザー定義コピーコンストラクターを宣言し、に向ける罪を犯すことconst_cast
です。
内部データへの直接参照を提供したCOWコンテナは、構築時にディープコピーする必要があります。そうしないと、参照カウントハンドルとして動作する可能性があります。
COWは最適化手法ですが、コピーコンストラクターのこのロジックは、正しく実装するために重要です。そのため、次に進む「最適化」セクションではなく、ここにこのケースを配置しました。
次の場合、最適化の懸念から独自のコピーコンストラクターを定義する必要があります。
要素の削除操作をサポートするコンテナを検討してください。ただし、削除された要素に削除済みのマークを付けるだけでサポートでき、後でスロットをリサイクルできます。このようなコンテナのコピーを作成する場合、「削除された」スロットをそのまま保持するのではなく、残っているデータを圧縮する方が理にかなっている場合があります。
オブジェクトには、その監視可能な状態の一部ではないデータが含まれている可能性があります。通常、これは、オブジェクトによって実行される特定の低速クエリ操作を高速化するために、オブジェクトの存続期間にわたって蓄積されたキャッシュ/メモ化されたデータです。関連する操作が実行されたときに(そしてもしそうなら!)再計算されるので、そのデータのコピーをスキップしても安全です。このデータのコピーは、オブジェクトの監視可能な状態(キャッシュされたデータの派生元)が変更操作によって変更されるとすぐに無効になる可能性があるため、不当になる可能性があります(オブジェクトを変更しない場合は、なぜディープを作成するのですか?次にコピーしますか?)
この最適化は、補助データが観測可能な状態を表すデータと比較して大きい場合にのみ正当化されます。
C ++では、コピーコンストラクターを宣言することで暗黙的なコピーを無効にできますexplicit
。その場合、そのクラスのオブジェクトを関数に渡したり、関数から値で返すことはできません。このトリックは、軽量に見えるが実際にコピーするのに非常に費用がかかるタイプに使用できます(ただし、準コピー可能にすることをお勧めします)。
C ++ 03では、コピーコンストラクターを宣言するには、それも定義する必要がありました(もちろん、それを使用する場合)。したがって、単に議論されている懸念からそのようなコピーコンストラクターを選ぶということは、コンパイラーが自動的に生成するのと同じコードを書かなければならないことを意味しました。
C ++ 11以降の標準では、デフォルトの実装を使用する明示的な要求を使用して、特別なメンバー関数(デフォルトおよびコピーコンストラクター、コピー代入演算子、およびデストラクタ)を 宣言できます(宣言をで終了するだけです
=default
)。
この答えは次のように改善できます。
- サンプルコードを追加する
- 「内部相互参照のあるオブジェクト」の場合を説明する
- リンクを追加する
以下のコードスニペットについて考えてみましょう。
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
データを明示的にコピーするために記述されたコードなしで作成されたユーザー定義のコピーコンストラクターがあるため、ジャンク出力を提供します。したがって、コンパイラは同じものを作成しません。
ほとんどの人はすでに知っていますが、この知識を皆さんと共有することを考えただけです。
乾杯...ハッピーコーディング!!!