C++ のコピーコンストラクターと代入演算子
C++ でクラスを作ったり、メンバーを追加した場合、必ずやらなければならないことがあります。 それは 「コピーコンストラクター と 代入演算子 が必要か適切に判断する」ということです。 また、必要だったとして、これらを書く場合にも様々な注意点やテクニックがあります。 今回はコピーコンストラクターと代入演算子に関する話題についてまとめてみたいと思います。
なお、今回はEffective C++の影響をかなり受けています。
Effective C++と同じような内容を書くと問題あるかと思い、 それらに関してはサワリと本の項の参照先だけ書いています。
(項は第 2 版のものです)
コピーコンストラクター、代入演算子 とは
まず、この 2 つについて、概要を説明します。コピーコンストラクターは 同じクラスのオブジェクトを引数にとるコンストラクター を指します。
Foo a; Foo b(a); // コピーコンスタクター代入演算子は 同じクラスのオブジェクトを代入する時 に使われます。
Foo a, c; c = a; // 代入演算子この 2 つは同じようなことをやるので、基本的にセットで考えます。 最初(初期化時)にコピーするのがコピーコンストラクターで、途中でコピーするのが代入演算子です。
ちょっと意外かもしれませんが、初期化時には代入に見えてもコピーコンストラクターが呼ばれます。
Foo a; Foo d = a; // コピーコンスタクターコピーコンスタクター、代入演算子の 2 つには大きなポイントがあります。
- 定義しなくても使える(デフォルトのものが呼び出される)
- デフォルトのものだと問題が生じることがある
定義方針
前章のポイントを踏まえてると、コピーコンストラクターと代入演算子を定義する方針は次のようになります。
なぜこういう方針になるのか、定義時の注意事項も含め、順に解説していきます。
デフォルトの仕様
コピーコンストラクター、代入演算子は記述しなくても、コンパイラーが自動で作成してくれるため、 コピー、代入を行うことができます。このことを知らないのか、知っててもちゃんと判断できないのか、 やたら書く人がいます。
しかし、これはよくありません。 時間の無駄ですし、メンバーの追加や継承を行った際に忘れてしまう危険性が出てきます。 不必要なコピーコンストラクター、代入演算子は定義しないようにしなければなりません。
不必要かどうか判断するためには、デフォルトで作成されるコピーコンストラクター、代入演算子 がどういうものか知っておく必要があります。
デフォルトで作成されるものでも、memcpy のような単純なコピーではありません。 「各メンバーに対して、コピーコンストラクターあるいは代入演算子を順に呼び出す」 という仕様になっています。
Effective C++
(45 項) C++ がどんな関数を黙って書き、呼び出しているか知っておこう
noncopy_sample.cpp :
class Person { private: std::string m_name; int m_age; // : };int など基本型のメンバーに関してはデフォルトのもので十分なのは、すぐわかると思います。
string のクラス自体は内部に動的に割り当てるメモリーを持っています。 しかし、string がコピーコンストラクター、代入演算子を持っていて、 それを呼び出すため、問題なく処理されます。
よって、このクラスではコピーコンストラクター、代入演算子の定義は不要です。
コピーコンストラクター、代入演算子の定義が必要な場合
定義が必要なのはメモリーを動的に割り当てるクラスです。 これはポインターのコピーでは同じものを指すために生じる問題です。
Effective C++
(11 項) メモリを動的に割り当てるクラスでは、コピーコンストラクタと代入演算子を宣言しよう
コピーコンストラクター、代入演算子の定義方法
コピーコンストラクターと代入演算子の定義の書式は次のようになります。C(const C &); C &operator=(const C &);具体的なサンプルをあげます。
copy_sample.cpp : (抜粋)
class Person { char *m_name; ///< 名前 unsigned int m_age; ///< 年齢 public: /// コンストラクター Person(const char *name = 0, unsigned int age = 0) :m_name(0), m_age(age) { SetName(name); } /// デストラクター. virtual ~Person() { SetName(0); } /// コピーコンストラクター Person(const Person &other) :m_age(other.m_age) { SetName(other.m_name); } Person &operator=(const Person &other) { // 自身の代入チェック if (this != &other) { SetName(other.m_name); m_age = other.m_age; } return *this; } // : };コピーコンストラクターと代入演算子を書く上でいくつか注意点があります。
代入演算子の戻り値は自身の参照
代入演算子では
*this
を返してます。
Person &operator=(const Person &other) { // : return *this; }ここは void でも実装は可能です。 ただ、そうすると基本型のように
a = b = 1;
といった連続した代入ができなくなリます。
そのため、自身の参照(*this)を返すのが流儀です。
Effective C++
(15 項) operator = を書くときは、*this へのリファレンスを返そう
代入演算子では自身の代入チェック
サンプルで this
かどうかをチェックしている部分です。
Person &operator=(const Person &other) { // 自身の代入チェック if (this != &other) { // : } return *this; }コピーコンストラクターとは違い、代入では自身が渡される可能性があります。 代入演算子を定義した場合には必ず自身のチェックを入れる習慣をつけた方がよいです。
Effective C++
(17 項) operator = では、自分自身へ代入するケースをチェックしよう
継承クラスでのコピー、代入
コピーコンストラクター、代入演算子で間違えやすいのが、 継承したクラスの場合です。copy_sample.cpp : (抜粋)
class Student : public Person { char *m_school; ///< 学校名 public: /// コンストラクター Student(const char *name = 0, unsigned int age = 0, const char *school = 0) :Person(name, age) { SetSchool(school); } /// デストラクター. virtual ~Student() { SetSchool(0); } /// コピーコンストラクター Student(const Student &other) :Person(other) { SetSchool(other.m_school); } Student &operator=(const Student &other) { // 自身の代入チェック if (this != &other) { // 基底クラスメンバーの代入 Person::operator=(other); SetSchool(other.m_school); } return *this; } // : };コピーコンストラクターでは初期化子で基底クラスのコピーコンストラクターを指定する必要があります。 これを忘れると基底クラスのメンバーはデフォルトコンストラクターで初期化されてしまいます。
Student(const Student &other) :Person(other) // 基底クラスのコピーコンスタクター {代入演算子でも明示的に基底クラスの代入演算子を呼び出す必要があります。
Student &operator=(const Student &other) { // : // 基底クラスメンバーの代入 Person::operator=(other); SetSchool(other.m_school);
Effective C++
(16 項) operator = では、すべてのデータメンバに代入しよう
代入演算子によるコピーコンストラクター実装の是非
DRY(Don't Repeat Yourself)原則とあるように、同じことを繰り返さないのがいいプログラミングです。コピーコンストラクター、代入演算子は同じような処理を行います。 コピーコンストラクターを代入演算子を使って実装することを考えてみます。
Person(const Person &other) { *this = other; }個人的にはこれも 「悪くはない」と思います。
ただ、これだとデフォルトでの初期化の後、代入となります。 そのため、メンバーの初期化演算子で設定した方が、若干速くなるはずです。 コピーコンストラクターも初期化子を使って別途、実装した方がよりよいです。
また、メンバーが const やリファレンスとなっている場合、 それらはそもそもコンストラクターでしか設定できません。 そういった場合は代入の方は後述の「禁止」にすることが多いと思います。
Effective C++
(12 項) コンストラクタでは、代入よりも初期化を使おう
コピー、代入の禁止
通常のメンバー関数は、定義しなければ使えません。 しかし、コピーコンストラクター、代入演算子は書かなくても自動で作成されます。 そのため、使えないようするには明示的に「禁止」する必要があります。コピー、代入の禁止が必要なケース
前章のコピーコンストラクター、代入演算子の定義が必要な場合だったとしても、 アプリケーションのクラスではコピー、代入の処理自体が必要ない場合も結構あります。 こういった場合はわざわざ実装せず、コピー、代入の禁止にします。代入できないメンバーを持つ場合も代入を禁止します。代入できないメンバーは次の 2 つです。
- const
- リファレンス
コピー、代入してはいけないメンバーを持つ場合もあります。
例えば、fstream やファイルポインター(FILE *)のようなファイルを扱う変数です。 これらはコピーして複数で使うとおかしなことになります。
なお fstream のように次節以降の方法でコピー、代入が禁止され、使えないようになっていることもあります。
禁止方法 - 基本
コンスタクターが自動で作ってしまう関数を禁止するには 関数を private にします。Effective C++
(27 項) 暗黙のうちに生成される不要なメンバ関数は、明示的に使用を禁止しよう
class Person { private: // コピー禁止 Person(const Person &); void operator=(const Person &);ここで代入演算子の戻り値を void にしていますが、 「戻り値だけ違う関数は定義できない」ため、なんでもかまいません。
禁止方法 - マクロ
コピー、代入を禁止するクラスを作ることは結構あります。 毎回書くのが面倒だったり、禁止する目的を明確にしたりするため、 次のようなマクロを使うこともあります。/// コピー禁止マクロ /// @param C クラス名 #define NON_COPYABLE(C) C(const C &); \ void operator=(const C &)これを使う場合、 このマクロを private 領域に記述します。
class Person { private: // コピー禁止(マクロ版) NON_COPYABLE(Person);ただ、C++ ではマクロはあまり推奨されていませんし、 マクロ自体をコーディング規約で禁止されていることもあります。
Effective C++
(27 項) #define ではなく、const と inline を使おう
禁止方法 - 継承 (boost::noncopyable)
直接書かなくても、private な継承を利用して関数を private にすることも可能です。 そのためのクラスを用意しておけば、マクロのように記述を簡略化、明確化することができます。Boost では、すでにそのクラスが用意されており、すぐに使うことができます。
#include <boost/noncopyable.hpp> class Person : boost::noncopyable {ただし、boost::noncopyable は使えない場合があります。
それは 他のクラスを継承しなければならない場合です。 その場合、多重継承で noncopyable を継承することになりますが、 boost::noncopyable は多重継承に対応していません。
多重継承に対応しようとするともう少し複雑になります。 ここまでやるなら、もう直接書くかマクロでもいいかなとは思います。
禁止方法 - C++11
C++11 では、関数の使用を禁止する delete 指定 の機能が追加されました。 これを使って、コピーコンストラクター、代入演算子を禁止すると次のようになります。class Person { public: // コピー禁止 (C++11) Person(const Person &) = delete; Person &operator=(const Person &) = delete;マクロにすると以下のような感じです。
/// コピー禁止マクロ (C++11) /// @param C クラス名 #define NON_COPYABLE(C) C& operator=(const C&) = delete; \ C(const C&) = delete;従来の方法と違い、 private 領域に書かないといけないという縛りがなくなります。
また、コンパイル時のエラーメッセージも「private なメソッドを使っている」から「削除されたメソッドを使っている」という旨のメッセージに変わり、原因が分かりやすくなっています。
なお、実際に C++11 を使う場合は移動コンストラクター、移動代入演算子もも考慮した方がいいのです。 そちらについては以下の記事をご覧ください。
サンプルコード
コピー禁止のサンプルコードです。SonarQube, C++ プラグインの Windows へのインストールと使い方
重複コードなどソースコードのメトリクスを調べてくれる SonarQube (旧 Sonar) というプログラムがあります。
今回はこの SonarQube の Windows へのインストールと C++ での使用法について説明したいと思います。

SonarQube とは
SonarQube はメトリクスツールで、 ソースコードを解析して各種の計測情報を表示してくれます。対応言語
もともと Java 用ですが、プラグインでその他の言語にも使えるようになります。 SonarQube 自体はフリーなのですが、プラグインの言語によっては有料のものもあり、 C/C++ 言語用のプラグインも有料です。ただし、サードパティーとして無料の Community 版もあるので、 今回はそちらでの C++ コードの解析について紹介します。
システム構成
SonarQube 本体は Web サービスのシステムで、計測結果は Web 上で表示されます。計測は SonarQube 用のスキャナーを使って行い、それをデータベース(DB)に保存します。 その DB 上のデータを SonarQube 本体で表示するという構成になっています。

スキャナーとして、今回はコマンドラインツールを使いますが、 Jenkins との連携用など様々な用途に合わせたものも用意されています。
メトリクス結果
実際の計測結果は次のような感じです。
すぐ使える計測結果は 3 つです。
- コード行数など
- ファイルの行数やクラス、メソッド、ステートメント(ステップ)数などの計測結果です。
ファイル(クラス)の行数は長すぎるのはよくありません。上限をコーディングルールで決められることも多いです。 個人的にはキリがいいので、 1 ファイル 1,000 行 以内としています。 - Duplication(重複コード率)
- コピペコード、コードクローンとも呼ばれるもので、同じようなコードがどのくらいあるかという数値です。
- 複雑度
- 深いネスティングによって上がります。これは基本的に少ない方が分かりやすいコードです。
DRY 原則もあるように重複しているところを修正する場合、同じような修正を何度もすることになります。 そのため、変更時にそのまま修正するか、作り直した方が早いかの指標として使われることもあります。
言い過ぎを恐れずに言えば、「プロジェクトの崩壊率」、「コーディングのヘタクソ度」といってもいいでしょう。
ただし、重複コードの計測は他のものと違って、正確に出すのは難しいです。 数値が高いものは確かに高いのですが、低くても重複コードは多い可能性はあります。
品質チェック
私がそんなに使い込んでいないので、詳しくありませんが、 ルールを決めたり、CppCheck などと組み合わせたりして、品質の保証にも使えるようです。インストール
ここから Windows でのインストール方法について説明していきます。インストールの方法や使い方は主に以下のページを参考にしています。
本体
SonarQube では SonarQube 本体およびソースコード解析用の Scanner(runner) をインストールします。まず、本体をダウンロードします。
- http://www.sonarqube.org/downloads/

ダウンロードしたファイルを C:\sonarqube などに展開します。
ここでは C:\local\sonar に展開し、バージョン番号を取り除いたとします。 (C:\local\sonar\sonarqube)
スキャナー
スキャナーとして今回はコマンドラインで実行するものを使用します。コマンドライン用のスキャナーは以下のページからダウンロードします。

ダウンロードしたファイルを C:\sonar-runner などに展開します。
こちらも C:\local\sonar に展開し、バージョン番号を取り除いたとします。 (C:\local\sonar\sonar-runner)
起動、ログイン
本体の方を実行し、サーバーを起動させます。 実行コマンドは (インストールフォルダー)\bin\(環境名)\StartSonar.bat です。c:\>c:\local\sonar\sonarqube\bin\windows-x86-32\StartSonar.bat以下のアドレスにアクセスします。

「 Log In 」 する場合、管理ユーザーの初期設定は次のようになっています。
Login | Password |
---|---|
admin | admin |
日本語化
わかりやすいように Web の表示を日本語化します。 ただ、対応していない部分の方が多いので、気持ち程度です。一部のプラグインは Web 上から追加できます。 日本語化プラグインも Web からインストールできます。
管理ユーザーでログイン後、以下の順にたどって [Update Center] を開きます。
Administration → System → Update Center[Available] を選択すると利用可能なプラグインのリストが表示されます。
ただし、プロキシー環境ではリストに何も表示されません。その場合、次節の設定を行ってください。

リストにある [Japanese Pack] の [Install] を選択してインストールします。

この状態はまだ Install Pending です。 再起動するとインストールが完了し、 Web が日本語化されます。

プロキシー設定
プロキシーの設定は (インストールフォルダー)/conf/sonar.properties で設定します。ファイルはデフォルトのものがあるので、 該当箇所のコメントアウトを外して、プロキシのアドレスとポートをそれぞれ記述します。
#-------------------------------------------------------------------------------------------------- # UPDATE CENTER # Update Center requires an internet connection to request http://update.sonarsource.org # It is enabled by default. #sonar.updatecenter.activate=true # HTTP proxy (default none) http.proxyHost=foo.co.jp http.proxyPort=80変更後、再起動すると設定が反映されます。
C++ プラグイン
C++ 言語用のプラグインもインストールしておきます。 有料のものは Web 上からインストールできるのですが、無料版(Community)の方は別途ダウンロードしてインストールする必要があります。まず、 Web からプラグインの jar ファイルを取得します。

ダウンロードしたファイルを以下のフォルダーに置きます。
- (インストールフォルダー)/extensions/plugins

使い方
動作確認
動作確認用にサンプルをダウンロードして、適当なところに展開します。 まず、 Java のサンプルで試してみます。展開した
projects/languages/java/sonar-runner/java-sonar-runner-simple
に移動します。そこでコマンドラインのスキャナーを実行します。
ちなみにスキャナーを実行する際も本体は起動していないとエラーになります。
> c:/local/sonar/sonar-runner/bin/sonar-runner.bat
続いて C++ のサンプルです。 こちらは
projects/languages/cpp/cpp-sonar-runner
に移動します。C++ の場合はここにあるプロジェクト設定ファイル(sonar-project.properties)を修正する必要があります。
このファイルの、以下の言語の設定で "cpp" を "c++" に書きなおすか、行全体を削除します。
sonar.language=cpp変更後、 java の時と同様にコマンドラインのスキャナーを実行します。言語が変わっても実行するコマンドは同じです。
> c:/local/sonar/sonar-runner/bin/sonar-runner.bat実行後、 Web を見てみるとプロジェクトが追加されています。

プロジェクトファイルの書き方
サンプルではすでにあるものを使用しましたが、実際に自分のプログラムに対して SonarQube を使いたい場合は プロジェクトファイル(sonar-project.properties)を作成します。# プロジェクトの識別用キー sonar.projectKey=my:project # プロジェクト名 sonar.projectName=My project # プロジェクトのバージョン sonar.projectVersion=1.0 # ソースファイルのあるフォルダー # sonar-project.properties からの相対パス sonar.sources=. # 対象言語 sonar.language=c++ # ソースの文字コード sonar.sourceEncoding=UTF-8よく使用する項目を表にまとめました。 他の項目については SonarQube のドキュメントを参照してください。
項目名(Key) | 説明 |
---|---|
projectKey | プロジェクトを識別するためのキー。使用している環境で一意(ユニーク)にならなければならない。 使用可能文字は アルファベットの小文字、数字、'-'、'_'、'.'、':' の記号。ただし、数字のみは不可。 |
projectName | プロジェクト名。 Web 上でのプロジェクトの表示名となる。 |
projectVersion | プロジェクトのバージョン |
sources | ソースのあるフォルダー。プロジェクトファイルからの相対パスで指定 |
language | 対象言語。 省略化するとマルチ言語として処理 |
sourceEncoding | ソースファイルの文字コード |
SonarQube の 4.2 以上ではマルチ言語モードといって言語を指定する必要がなくなりました。 すべての言語で対応しているわけではありませんが、 C++ では対応しています。 ちなみに対象言語として C++ を明示的に指定する場合、 cpp とすると有料版の C++ のプラグインが呼ばれます。 Community 版の場合は c++ と書く必要があります。
また、 sourceEncoding でソースの文字コードは指定可能ですが、 プロジェクトファイル自体は BOM なし UTF-8 で記述する必要があります。
プロジェクトの削除
プロジェクトの追加はスキャナーを実行すると、自動的に追加されます。 最後に逆のプロジェクトの削除についても簡単に説明しておきますプロジェクトの削除は Web 上で行えます。
[設定] → [Projects] → [Management]上記の順でたどり、削除したいプロジェクト項目の [Delete] を選択すると削除されます。
