忘れやすいC++の仕様(2/3)

続いて、C++のクラスの仕様に関する、忘れていたことを書き留める。

(1) 定数オブジェクトは定数メソッドしか使えない
例:

class CLS{
    int i;
public:
    CLS(int i = 1){this->i = i;}
    int get() const {return i;}  /* (a) */
    void set(int i) {this->i = i;}
};

int main()
{
    CLS c1;
    const CLS c2(2);
    int i;

    c1.set(3);
    c2.set(4);    /* (b) */
    i = c1.get();
    i = c2.get();    /* (c) */
}
このソースをコンパイルすると、(b)でエラーになる。
もし(a)の行のconstを外すと、(c)もコンパイルエラーになる。コンパイル時にreadonlyとwritableの区別はメソッド呼び出し毎に厳密になされる言語仕様らしい。
定数オブジェクトが出現する可能性を考えると、クラス定義においてメソッドの定数定義は重要だ。

(2)デフォルトコンストラクタが無いと配列を定義できない

class CLS{
    int i;
public:
    CLS(int i){this->i = i;}    /* (d) */
};

int main()
{
    CLS c[2];    /* (e) */
}
このコードは、(e)がコンパイルエラーになる。
C++では、配列の定義時に引数のあるコンストラクタが呼べないので、引数なしで呼べるコンストラクタ(デフォルトコンストラクタ)が存在しないと、そのクラスの配列は定義できない。
従って、CLS::CLS()やCLS::CLS(int i=0)のようなメソッドが必要になる。

なお、このコードの(d)を削除すると、コンパイルが通る。明示的なコンストラクタ定義が1つも無ければ、自動的にデフォルトコンストラクタが作成されるからだ。

(3)単項演算子の定義方法
単項演算子には前置(++obj等)と後置(obj++等)があり、それぞれ定義方法が異なる。
クラス名をCLS、演算子を@とすると、前置はCLS::operator@() / operator@(CLS&)、後置はCLS::operator@(int) / operator@(CLS&, int)で定義する。呼び出し時には、int引数には0が入る。

また、単項演算子関数が返す型は、CLSでもCLS&でも良い。だから、通常は異なる値を生成する(一時的なインスタンスを作って返す)マイナス演算子が何もしないなら、

CLS& CLS::operator-() {return *this;}
と参照を返すこともできる。

(4) コピーコンストラクタと代入演算子の共通化
コピーコンストラクタを定義する必要がある場合、代入演算子もそっくりな内容で定義する必要がある場合が多い。
そんな代入演算子を定義しようとして、

class CLS{
private:
    …
public:
    CLS(const CLS& c){…}
    const CLS& operator=(const CLS& c){CLS(c); return *this;}  /* (f) */
};
のようにコピーコンストラクタを普通のメソッドと同じ感覚で呼び出そうとすると、コンパイルエラーになる。なぜなら、上記の"CLS(c);"は"CLS c;"と同じ意味(!)、つまり新たなインスタンスの定義になるからだ。
コピーコンストラクタと代入演算子の処理を共通化するには、次のように別な関数を用意する必要がある。
class CLS{
private:
    …
    inline void copy(const CLS& c){…}
public:
    CLS(const CLS& c){copy(c);}
    const CLS& operator=(const CLS& c){copy(c); return *this;}
};

(参考文献:JIS X 3014)