web-dev-qa-db-ja.com

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

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

78
penguru

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

_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;
}
_
68
sharptooth

Rule of Fiveのルールが引用されていないことを少し覗き見ています。

このルールは非常に簡単です。

5つのルール
Destructor、Copy Constructor、Copy Assignment Operator、Move Constructor、またはMove Assignment Operatorのいずれかを作成する場合は、おそらく他の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;
};

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

44
Matthieu M.

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

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


正しさ/意味

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

このセクションを読み終えると、コンパイラーが独自にコピーコンストラクターを生成できるようにするいくつかの落とし穴について学習します。したがって、 seandanswer に記載されているように、新しいクラスのコピー可能性をオフにして故意に有効にすることは常に安全です後で本当に必要になったときに。

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

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

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

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


浅いコピーとディープコピー

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

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

自己登録オブジェクト

どのように構築されたとしても、すべてのオブジェクトが何らかの形で登録されなければならないクラスを考えてください。いくつかの例:

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

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

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

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


内部相互参照を持つオブジェクト

一部のオブジェクトは、異なるサブオブジェクト間で直接相互参照する、自明ではない内部構造を持つ場合があります(実際、このような内部参照は1つだけでこのケースをトリガーできます)。コンパイラが提供するコピーコンストラクターは、内部intra-objectアソシエーションを破壊し、それらをinter-object関連付け。

例:

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?

特定の条件を満たすオブジェクトのみをコピーできます

ある状態(例:default-constructed-state)でオブジェクトがコピーしても安全なクラスと、そうでなければコピーしても安全なnotクラスがあるかもしれません。コピーしても安全なオブジェクトのコピーを許可したい場合-防御的にプログラミングする場合-ユーザー定義のコピーコンストラクターで実行時チェックが必要です。


コピー不可サブオブジェクト

コピー可能なクラスは、コピーできないサブオブジェクトを集約する場合があります。通常、これは、観測不可能な状態のオブジェクトに対して発生します(その場合については、以下の「最適化」セクションで詳しく説明します)。コンパイラは、単にそのケースを認識するのに役立ちます。


準コピー可能なサブオブジェクト

コピー可能なクラスは、準コピー可能なタイプのサブオブジェクトを集約する場合があります。準コピー可能な型は厳密な意味でコピーコンストラクターを提供しませんが、オブジェクトの概念的なコピーを作成できる別のコンストラクターを備えています。型を準コピー可能にする理由は、型のコピーセマンティクスについて完全な合意がない場合です。

たとえば、オブジェクトの自己登録のケースを再検討すると、オブジェクトが完全なスタンドアロンオブジェクトである場合にのみ、グローバルオブジェクトマネージャーにオブジェクトを登録する必要がある状況があると考えることができます。それが別のオブジェクトのサブオブジェクトである場合、それを管理する責任はそれを含むオブジェクトにあります。

または、浅いコピーと深いコピーの両方をサポートする必要があります(いずれもデフォルトではありません)。

次に、最終的な決定はそのタイプのユーザーに委ねられます-オブジェクトをコピーするとき、ユーザーは(追加の引数を介して)コピーの目的の方法を明示的に指定する必要があります。

プログラミングに対する非防御的なアプローチの場合、通常のコピーコンストラクターと準コピーコンストラクターの両方が存在する可能性もあります。これは、ほとんどの場合単一のコピー方法を適用する必要がある場合に正当化できますが、まれではありますが十分に理解されている状況では代替コピー方法を使用する必要があります。コンパイラーは、コピーコンストラクターを暗黙的に定義できないと文句を言いません。そのタイプのサブオブジェクトを準コピーコンストラクターを介してコピーする必要があるかどうかを覚えて確認するのは、ユーザーの唯一の責任です。


オブジェクトのアイデンティティに強く関連付けられている状態をコピーしないでください

まれに、オブジェクトのobservable状態のサブセットが、オブジェクトのIDの分離不可能な部分を構成(または考慮)し、他のオブジェクトに転送できないようにする必要があります(ただし、物議を醸す)。

例:

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

  • 新しいオブジェクトがソースオブジェクトの履歴を継承してはならず、代わりに単一の履歴項目 "Copied at <TIME>で始まる場合のオブジェクトの履歴(Undo/Redoスタックなど) <OTHER_OBJECT_ID>」から。

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


コピーコンストラクターの正しい署名を強制する

コンパイラが提供するコピーコンストラクターのシグネチャは、サブオブジェクトで使用可能なコピーコンストラクターによって異なります。少なくとも1つのサブオブジェクトに実際のコピーコンストラクターがない(定数参照によってソースオブジェクトを取得する)が、代わりにmutating copy-constructor(非定数参照によるソースオブジェクトの取得)コンパイラは、暗黙的に宣言し、mutating copy-constructorを定義する以外に選択肢がありません。

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


コピーオンライト(COW)

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

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



最適化

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


コピー中の構造最適化

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


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

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

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


暗黙のコピーを無効にする

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

C++ 03では、コピーコンストラクターの宣言にも定義する必要があります(もちろん、使用する場合)。したがって、このようなコピーコンストラクタを検討することは、単に議論の対象外であるため、コンパイラが自動的に生成するのと同じコードを記述する必要があることを意味します。

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



TODO

この回答は次のように改善できます:

  • サンプルコードの追加
  • 「内部相互参照のあるオブジェクト」の事例を示します
  • いくつかのリンクを追加
28
Leon

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

_title = new char[length+1]_を実行してからstrcpy(title, titleIn)を実行するコピーコンストラクタを作成する必要があります。コピーコンストラクタは、「浅い」コピーを実行します。

6
Peter Ajtai

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

2
josh

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

0
seand