読者です 読者をやめる 読者になる 読者になる

Flat Leon Works

アプリやゲームを作ってます。

【C++】メンバ関数には必要に応じてconstをつけよう

constメンバ関数

メンバ関数につけるconstとは何か。これです。

class A
{
public:
    int m_Value;
    void Hoge( void ) const // ←このconstです
    {
    }
};

メンバ関数の右側にconstをつけると、そのメンバ関数内ではメンバ変数の変更ができなくなります。このメンバ関数constメンバ関数と呼んだりもします。

constメンバ関数内では、メンバ変数を変更するような他の関数の呼び出しも禁止されます。

void SetZero( int& value )
{
    value = 0;
}

class A
{
public:
    int m_Value;
    void IncValue( void )
    {
        m_Value += 1;
    }
    void Hoge( void ) const
    {
        IncValue(); // メンバ変数を変更する関数なので呼び出し禁止(コンパイルエラー)
        SetZero( m_Value ); // メンバ変数を変更する関数なので呼び出し禁止(コンパイルエラー)
    }
};

挙動的には、constメンバ関数内ではthisはconstのthisポインタになります。つまり、クラスAではthisはconst A*になるというわけです。constポインタはそのメンバ変数の変更と、非constメンバ関数の呼び出しが禁止されます。

A a;
const A* pA = &a;
pA->m_Value = 1; // メンバ変数の変更をしているのでコンパイルエラー
pA->IncValue(); // 非constメンバ関数を呼び出しているのでコンパイルエラー

constメンバ関数のメリット

これの何が嬉しいのか。

  • 意図しないデータ変更を防ぐことができる -> バグの発生を減らす
  • メンバ関数がオブジェクトのデータ変更しないことをすぐに知ることができる -> ソースコードの可読性が上がる

バグの発生を減らし、ソースコードの可読性が上がるconstメンバ関数。使わない手はないです。

constメンバ関数の使いどころ

メンバ関数constにするかどうかは、そのメンバ関数が情報を取得するための関数なのか、情報を変更するための関数なのかですぐに決めることができます。情報を取得するための関数の場合はconstをつけましょう。たまに情報を取得しつつ、情報を変更する関数もあったりしますがその場合はconstはつけないようにします。

constメンバ関数内でもメンバ変数を変更したい場合

constメンバ関数内でメンバ変数を変更するためのmutableというキーワードがあります。これはメンバ変数の宣言時に頭に付けて使います。

class A
{
public:
    mutable int m_Value; // mutableを付けて宣言
    void IncValue( void )
    {
        m_Value += 1;
    }
    void Hoge( void ) const
    {
        IncValue(); // これは相変わらずダメ。thisはconstポインタのままなので。
        m_Value += 1; // m_Valueはmutable付きで宣言されているので変更可能。
    }
};

どういうときにmutableを使うのかというと、内部的な情報を変更するときに使います。たとえばキャッシュなどです。

class A
{
    const char* m_pName;
    mutable int m_NameLengthCache;
public:
    A( const char* p_name  ) : m_pName( p_name ), m_NameLengthCache( -1 ){}

    const char* GetName( void ) const { return m_pName; }
    void SetName( const char* p_name  ){ m_pName = p_name; m_NameLengthCache = -1; }
    
    int GetNameLength( void ) const
    {
        if ( m_NameLengthCache == -1 )
        {
            m_NameLengthCache = strlen( m_pName );
        }
        return m_NameLengthCache;
    }
};

クラスAのGetNameLengthメンバ関数内では最初の一回のみ名前の長さを計算してそれをメンバ変数に保存するようにしています。つまりキャッシュです。ここでポイントなのは、m_NameLengthCacheはクラスAの内部的な情報であり、利用者側には関係のない情報だということです。constメンバ関数内でm_NameLengthCacheが変更されたとしても利用者には関係がないので、クラスAのデータは変化していないとみなすことができます。

このようにconstはmutableによって、厳密にデータ変更の有無をあらわすのではなく、利用者から見たデータ変更の有無を表すものとして利用することが可能になっています。ただし、あまり多用すべきではありません。mutableはconstに例外を与えるものなので、多用するとconstの信頼性がなくなっていきます。キャッシュ用途ぐらいにとどめておいたほうがいいと思います。

C++11ではconstメンバ関数はスレッドセーフを保証するという意味も付加されるようになったらしいです[参考 ]。この記事でのキャッシュの例は、C++11以降だとconstメンバ関数の要件を満たさないことになります。注意してください。

constメンバ関数の注意点

constメンバ関数によって保証されるのはメンバ変数が変更されないことです。メンバ変数がポインタや参照の場合、その参照先は自由に変更できてしまいます。

class A
{
    int& m_Other;
    int* m_pOther;
public:
    A( int& other ) : m_Other( other ), m_pOther( &other ){}
    void Hoge( void ) const
    {
        m_Other = 1;
        *m_pOther = 1;
    }
};

constメンバ関数内でm_Other = 1;のようなことができてしまっています。これはメンバ変数の参照先の値を変更しているだけで、メンバ変数自体には変更を加えていないからです。メンバ変数の参照先もクラスAのデータであるとみなしたい場合は困る挙動です。これは注意するしかないです。

また、ポインタを外部に公開する場合も注意が必要です。

class A
{
    int& m_Other;
    int* m_pOther;
public:
    A( int& other ) : m_Other( other ), m_pOther( &other ){}
    int* GetOther( void ) const { return m_pOther; }
};

GetOtherメンバ関数にはconstがついていますが、その戻り値のポインタの中身を変更することができてしまいます。これもメンバ変数の参照先もクラスAのデータであるとみなしたい場合には困る挙動です。しかしこの場合にはよい対策があります。

class A
{
    int& m_Other;
    int* m_pOther;
public:
    A( int& other ) : m_Other( other ), m_pOther( &other ){}
    const int* GetOther( void ) const { return m_pOther; }
};

const付きにして返せばよいのです。こうすることでm_pOtherが指す先もクラスAのデータとみなすような挙動にすることができます。つまりAがconstの場合、m_pOtherが指す先もconstにすることができるのです。このconstメンバ関数でポインタをconstポインタにして返す方法はとてもよく使われる手法です。

constメンバ関数オーバーロード

同名のメンバ関数constありなしでオーバーロードすることができます。

class A
{
    int& m_Other;
    int* m_pOther;
public:
    A( int& other ) : m_Other( other ), m_pOther( &other ){}

    const int* GetOther( void ) const { return m_pOther; }
          int* GetOther( void )       { return m_pOther; } // constなしでオーバーロード
};

これを利用することで、オブジェクトがconstかどうかで返す値もconstかどうかが変わるゲッター関数を作ることができます。

int value;
A a( value );
A* pA = &a;
const A* pA2 = &a;
*(pA->GetOther()) =0; // pAは非constポインタなのでGetOtherの返却値も非constポインタになる(参照先の中身を変更できる)
/*  エラー
*(pA2->GetOther()) =0; // pA2はconstポインタなのでGetOtherの返却値もconstポインタになる(参照先の中身を変更できない)
*/

また、このゲッター関数をconst版のみを公開するようにすれば、取得のみ可能で変更不可なゲッターにすることができます。 このconstメンバ関数オーバーロードを利用したゲッター関数の定義は、先ほどの「ポインタをconstポインタにして返す手法」と合わせてよく使われる手法です。覚えておきましょう。

constメンバ関数を使うかどうかでソースコード全部に影響がでる

constメンバ関数は絶対に使うべき存在ではあるのですが、もし今まで使っていなくてこれから使おうと思っているのであれば今すぐ使いはじめるべきです。なぜならconstメンバ関数は部分的に使うということができず、一部で使い始めるとすべてのソースコードconstに対応する必要がでてくるからです。つまり使い始めるのが遅れれば遅れるほどconst対応が大変になります。今すぐ使い始めましょう。