いつコピーコンストラクターを使用する必要がありますか?


87

C ++コンパイラがクラスのコピーコンストラクタを作成することを知っています。その場合、ユーザー定義のコピーコンストラクターを作成する必要がありますか?いくつか例を挙げていただけますか?



1
独自のcopy-ctorを作成する場合の1つ:ディープコピーを実行する必要がある場合。また、ctorを作成するとすぐに、デフォルトのctorが作成されないことに注意してください(defaultキーワードを使用しない限り)。
harshvchawla 2017

回答:


75

コンパイラーによって生成されたコピーコンストラクターは、メンバーごとのコピーを行います。時にはそれだけでは不十分です。例えば:

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;
}

10
ビット単位では実行されませんが、特にクラス型メンバーのcopy-ctorを呼び出すメンバー単位のコピーが実行されます。
Georg Fritzsche 2010

7
そのようなアッシング演算子を書かないでください。その例外安全ではありません。(newが例外をスローした場合、オブジェクトは未定義の状態のままになり、ストアはメモリの割り当て解除された部分を指します(スローできるすべての操作が正常に完了した後にのみメモリの割り当てを解除します))。簡単な解決策は、コピースワップイディウムを使用することです。
マーティンヨーク

@sharptooth下から3行目、delete stored[];そうあるべきだと思いますdelete [] stored;
Peter Ajtai 2010

4
これは単なる例ですが、より良い解決策はを使用することstd::stringです。一般的な考え方は、リソースを管理するユーティリティクラスのみがビッグスリーをオーバーロードする必要があり、他のすべてのクラスはそれらのユーティリティクラスを使用するだけで、ビッグスリーを定義する必要がなくなるというものです。
GManNickG 2010

2
@マーティン:石に刻まれていることを確認したかった。:P
GManNickG 2010

46

のルールが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 Operatorunique_ptr

したがって、sharptoothのクラスの再検討:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

私はあなたのことを知りませんが、私は私の方が簡単だと思います;)


C ++ 11の場合-ムーブコンストラクターとムーブ代入演算子の3つのルールに追加される5つのルール。
Robert Andrzejuk 2017年

1
@Robb:実際には、最後の例で示したように、通常はRule OfZeroを目指す必要があることに注意してください。専門の(一般的な)技術クラスのみが1つのリソースの処理に注意を払う必要があり、他のすべてのクラスはそれらのスマートポインター/コンテナーを使用し、それについて心配する必要はありません。
Matthieu M.

@MatthieuM。同意しました:-)この答えはC ++ 11より前であり、「ビッグスリー」で始まるため、ルールオブファイブについて言及しましたが、現在は「ビッグファイブ」が関連していることに注意してください。尋ねられた文脈では正しいので、私はこの答えに反対票を投じたくありません。
Robert Andrzejuk 2017年

@Robb:良い点です。答えを更新して、BigThreeではなくRuleofFiveについて言及しました。うまくいけば、ほとんどの人が今までにC ++ 11対応のコンパイラに移行しました(そして、まだ移行していない人は残念です)。
Matthieu M.

32

私の練習から思い出して、コピーコンストラクターを明示的に宣言/定義する必要がある場合の次のケースを考えることができます。ケースを2つのカテゴリに分類しました

  • 正確性/セマンティクス-ユーザー定義のコピーコンストラクターを提供しない場合、そのタイプを使用するプログラムはコンパイルに失敗するか、正しく動作しない可能性があります。
  • 最適化-コンパイラーが生成したコピーコンストラクターの優れた代替手段を提供することで、プログラムを高速化できます。


正しさ/意味論

このセクションでは、コピーコンストラクターを宣言/定義することが、そのタイプを使用するプログラムの正しい操作に必要な場合を示します。

このセクションを読んだ後、コンパイラーが独自にコピーコンストラクターを生成できるようにすることのいくつかの落とし穴について学びます。したがって、seandが彼の回答で述べたように、新しいクラスのコピー可能性をオフにし、後で本当に必要なときに意図的に有効にすることは常に安全です。

C ++ 03でクラスをコピー不可にする方法

プライベートコピーコンストラクターを宣言し、その実装を提供しないでください(そのため、そのタイプのオブジェクトがクラス自体のスコープまたはその友人によってコピーされた場合でも、リンク段階でビルドが失敗します)。

C ++ 11以降でクラスをコピー不可にする方法

=delete最後にコピーコンストラクタを宣言します。


浅いコピーと深いコピー

これは最もよく理解されているケースであり、実際には他の回答で言及されている唯一のケースです。shaprtoothはそれをかなりうまくカバーしています。オブジェクトによって排他的に所有されるべきである深くコピーするリソースは、動的に割り当てられたメモリが1種類にすぎない、あらゆるタイプのリソースに適用できることだけを追加したいと思います。必要に応じて、オブジェクトを深くコピーすることも必要になる場合があります

  • ディスク上の一時ファイルのコピー
  • 別のネットワーク接続を開く
  • 別のワーカースレッドを作成する
  • 別のOpenGLフレームバッファを割り当てる

自己登録オブジェクト

すべてのオブジェクトが(どのように構築されていても)何らかの方法で登録する必要があるクラスについて考えてみます。いくつかの例:

  • 最も単純な例:現在存在するオブジェクトの総数を維持する。オブジェクトの登録は、静的カウンターをインクリメントすることです。

  • より複雑な例は、シングルトンレジストリを使用することです。このレジストリには、そのタイプの既存のすべてのオブジェクトへの参照が格納されます(通知をすべてのオブジェクトに配信できるようにするため)。

  • 参照カウントされたスマートポインタは、このカテゴリの特殊なケースと見なすことができます。新しいポインタは、グローバルレジストリではなく、共有リソースに「登録」されます。

このような自己登録操作は、そのタイプの任意のコンストラクターによって実行される必要があり、コピーコンストラクターも例外ではありません。


内部相互参照のあるオブジェクト

一部のオブジェクトは、異なるサブオブジェクト間で直接相互参照する重要な内部構造を持っている場合があります(実際、このような内部相互参照は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に強く関連付けられている状態をコピーしないでください

まれに、オブジェクトの観察可能な状態のサブセットが、オブジェクトのIDの不可分の一部を構成する(または見なされる)場合があり、他のオブジェクトに転送できないようにする必要があります(これは多少物議を醸す可能性があります)。

例:

  • オブジェクトのUID(ただし、IDは自己登録の動作で取得する必要があるため、これも上記の「自己登録」の場合に属します)。

  • 新しいオブジェクトがソースオブジェクトの履歴を継承してはならず、代わりに単一の履歴アイテム「<OTHER_OBJECT_ID>から<TIME>にコピーされた」で開始する必要がある場合のオブジェクトの履歴(たとえば、元に戻す/やり直しスタック)。

このような場合、コピーコンストラクタは対応するサブオブジェクトのコピーをスキップする必要があります。


コピーコンストラクターの正しい署名の実施

コンパイラーが提供するコピーコンストラクターの署名は、サブオブジェクトで使用できるコピーコンストラクターによって異なります。少なくとも1つのサブオブジェクトに実際のコピーコンストラクター(定数参照によってソースオブジェクトを取得)がなく、代わりに変更コピーコンストラクター(非定数参照によってソースオブジェクトを取得)がある場合、コンパイラーは選択の余地がありません。ただし、変更するコピーコンストラクタを暗黙的に宣言してから定義します。

では、サブオブジェクトの型の「変更」コピーコンストラクターが実際にソースオブジェクトを変更しない場合(そして、constキーワードを知らないプログラマーによって作成された場合)はどうなるでしょうか。不足しているを追加してそのコードを修正できない場合const、他のオプションは、正しい署名を使用して独自のユーザー定義コピーコンストラクターを宣言し、に向ける罪を犯すことconst_castです。


コピーオンライト(COW)

内部データへの直接参照を提供したCOWコンテナは、構築時にディープコピーする必要があります。そうしないと、参照カウントハンドルとして動作する可能性があります。

COWは最適化手法ですが、コピーコンストラクターのこのロジックは、正しく実装するために重要です。そのため、次に進む「最適化」セクションではなく、ここにこのケースを配置しました。



最適化

次の場合、最適化の懸念から独自のコピーコンストラクターを定義する必要があります。


コピー中の構造最適化

要素の削除操作をサポートするコンテナを検討してください。ただし、削除された要素に削除済みのマークを付けるだけでサポートでき、後でスロットをリサイクルできます。このようなコンテナのコピーを作成する場合、「削除された」スロットをそのまま保持するのではなく、残っているデータを圧縮する方が理にかなっている場合があります。


観察不可能な状態のコピーをスキップする

オブジェクトには、その監視可能な状態の一部ではないデータが含まれている可能性があります。通常、これは、オブジェクトによって実行される特定の低速クエリ操作を高速化するために、オブジェクトの存続期間にわたって蓄積されたキャッシュ/メモ化されたデータです。関連する操作が実行されたときに(そしてもしそうなら!)再計算されるので、そのデータのコピーをスキップしても安全です。このデータのコピーは、オブジェクトの監視可能な状態(キャッシュされたデータの派生元)が変更操作によって変更されるとすぐに無効になる可能性があるため、不当になる可能性があります(オブジェクトを変更しない場合は、なぜディープを作成するのですか?次にコピーしますか?)

この最適化は、補助データが観測可能な状態を表すデータと比較して大きい場合にのみ正当化されます。


暗黙的なコピーを無効にする

C ++では、コピーコンストラクターを宣言することで暗黙的なコピーを無効にできますexplicit。その場合、そのクラスのオブジェクトを関数に渡したり、関数から値で返すことはできません。このトリックは、軽量に見えるが実際にコピーするのに非常に費用がかかるタイプに使用できます(ただし、準コピー可能にすることをお勧めします)。

C ++ 03では、コピーコンストラクターを宣言するには、それも定義する必要がありました(もちろん、それを使用する場合)。したがって、単に議論されている懸念からそのようなコピーコンストラクターを選ぶということは、コンパイラーが自動的に生成するのと同じコードを書かなければならないことを意味しました。

C ++ 11以降の標準では、デフォルトの実装を使用する明示的な要求を使用して、特別なメンバー関数(デフォルトおよびコピーコンストラクター、コピー代入演算子、およびデストラクタ)を 宣言できます(宣言をで終了するだけです=default)。



TODO

この答えは次のように改善できます。

  • サンプルコードを追加する
  • 「内部相互参照のあるオブジェクト」の場合を説明する
  • リンクを追加する

6

コンテンツを動的に割り当てたクラスがある場合。たとえば、本のタイトルをchar *として保存し、タイトルをnewに設定すると、コピーは機能しません。

title = new char[length+1]その後、を実行するコピーコンストラクターを作成する必要がありstrcpy(title, titleIn)ます。コピーコンストラクタは、「浅い」コピーを実行するだけです。


2

コピーコンストラクタは、オブジェクトが値によって渡されるか、値によって返されるか、または明示的にコピーされるときに呼び出されます。コピーコンストラクターがない場合、c ++は浅いコピーを作成するデフォルトのコピーコンストラクターを作成します。オブジェクトに動的に割り当てられたメモリへのポインタがない場合は、シャローコピーで十分です。


0

クラスで特に必要とされない限り、copyctorとoperator =を無効にすることをお勧めします。これにより、参照が意図されているときに値で引数を渡すなどの非効率性を防ぐことができます。また、コンパイラが生成したメソッドが無効である可能性があります。


-1

以下のコードスニペットについて考えてみましょう。

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();データを明示的にコピーするために記述されたコードなしで作成されたユーザー定義のコピーコンストラクターがあるため、ジャンク出力を提供します。したがって、コンパイラは同じものを作成しません。

ほとんどの人はすでに知っていますが、この知識を皆さんと共有することを考えただけです。

乾杯...ハッピーコーディング!!!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.