Modern C++でのクラスに関する私的メモ


このエントリーをはてなブックマークに追加

C++のクラスについて理解があやふやなので、調べたことを以下にまとめます。C++11以降をターゲットにしています。

特殊メンバ関数 (Special member functions)

以下に、メンバ変数を2つとメンバ関数を1つのみ持つ単純なクラスを含むサンプルを示します。

#include <iostream>
#include <cstring>

class Person {
private:
    int m_age;
    std::string m_name;

public:
    void setMembers(int age, std::string name) {
        m_age = age;
        m_name = name;
    }
};

int main() {
    Person obj1;  // デフォルトコンストラクタ
    obj1.setMembers(30,  "George");    

    Person obj2(obj1);             // コピーコンストラクタ
    Person obj3 = obj1;            // コピーコンストラクタ
    obj3 = obj1;                   // コピー代入演算子
    auto obj4 = std::move(obj1); // ムーブコンストラクタ
    obj1 = std::move(obj4);        // ムーブ代入演算子

    return 0;
}

Personクラスには必要最低限しか記述されていないにもかかわらず、Personクラスのオブジェクトに対して様々な処理を行えるのは、特殊メンバ関数(Special Member Functions)と呼ばれる、以下の6つの特別なメンバ関数が暗黙的に定義されるおかげです。

特殊メンバ関数が暗黙的に定義されない場合

ややこしいことに、ある関数をプログラマが明示的に定義すると、上記Special Member Functionのうちのいくつかが、暗黙的に定義されなくなる仕様になっています。

有名なところでは、もし引数を一つ以上もつコンストラクタが明示的に定義された場合、デフォルトコンストラクタは暗黙的に定義されなくなります。

以下にその例を示します。引数付きコンストラクタを明示的に定義しているせいでデフォルトコンストラクタが作られないので、コンパイルが通りません。

#include <iostream>
#include <cstring>

class Person {
private:
    int m_age;
    std::string m_name;

public:
    Person(int age, std::string name)
        : m_age(age)
        , m_name(name) {
    }
};

int main() {
    Person obj1; // エラー! デフォルトコンストラクタが存在しない

    return 0;
}

コンパイルを通すには2つの方法があります。1つ目は、defaultキーワードを使ってデフォルトコンストラクタを明示的に作らせる方法です。これはC++11以降可能になった方法です。

class Person {
private:
    int m_age;
    std::string m_name;

public:
    Person(int age, std::string name)
        : m_age(age)
        , m_name(name) {
    }

    Person() = default;
}

2つ目は、従来通りの定義方法です。

class Person {
private:
    int m_age;
    std::string m_name;

public:
    Person(int age, std::string name)
        : m_age(age)
        , m_name(name) {
    }

    Person() : m_age(-1), m_name("no name") {};
};

特殊メンバ関数が暗黙的に定義されない場合の詳細については、Special member functions - Wikipedia, the free encyclopedia や、 これを見やすい表にした 特殊メンバ関数とコンパイラによる暗黙宣言 - yohhoyの日記 にまとまっています。

Rule of Three

英語圏C++に関する記事を読むと、しばしば「Rule of Three」という用語が出てきます。これは、「デストラクタ、コピーコンストラクタ、コピー代入演算子のうち少なくとも1つをプログラマが明示的に定義したのであれば、残りについても、プログラマが明示的に定義しなければならない可能性が高い」という経験則を表す標語のようです。(参考:Rule of three (C++ programming) - Wikipedia, the free encyclopedia), c++ - What is The Rule of Three? - Stack Overflow

上で述べた関数のうちの一つをプログラマが明示的に定義したということは、例えばリソース管理などセンシティブな処理をクラス内で行っている可能性が高いです。ということは、他の関数についても明示的に定義して適切な処理を行わないと、リソース漏れなどが発生する可能性が高い、といえます。

上記3関数のうちの1つが明示的に定義されたら、残りの2つの関数を明示的に定義しないようにコンパイラが強制してくれればよいのですが、現状ではそうなっていません。そのため、プログラマは、上記3関数のいずれも明示的に定義しない(コンパイラの自動生成に任せる)か、もしくは上記3関数とも明示的に定義するかのどちらかを、責任をもって行うのがよいプラクティスになります。