閉じる

【C++】constメンバ関数の使い方を徹底解説!mutableや注意点も

C++におけるクラス設計において、オブジェクトの状態を変更しないことを保証するconstメンバ関数は非常に重要な役割を果たします。

単なる読み取り専用のマークではなく、コードの安全性(堅牢性)を高め、プログラムの意図を明確にするための不可欠な仕組みです。

本記事では、基本的な使い方から、メンバ関数のオーバーロード、さらには例外的な変更を許可するmutableの活用方法まで、プロフェッショナルな開発に役立つ知識を詳しく解説します。

constメンバ関数とは?

C++のconstメンバ関数とは、その関数を呼び出してもオブジェクトの内部状態(メンバ変数)を変更しないことをコンパイラに対して約束する仕組みです。

この指定を行うことで、読み取り専用のオブジェクトに対してもその関数を実行できるようになります。

構文と基本的な役割

constメンバ関数を定義するには、関数の引数リストの直後にconstキーワードを記述します。

これにより、その関数内ではメンバ変数がすべて「読み取り専用」として扱われます。

C++
class User {
private:
    std::string name;

public:
    User(std::string n) : name(n) {}

    // constメンバ関数:名前を取得するだけで変更はしない
    std::string getName() const {
        // name = "New Name"; // ここで代入しようとするとコンパイルエラーになる
        return name;
    }

    // 通常のメンバ関数:名前を変更する
    void setName(std::string n) {
        name = n;
    }
};

なぜconstメンバ関数が必要なのか

constメンバ関数を適切に設定する最大の理由は、const指定されたオブジェクト(定数オブジェクト)からでも呼び出せるようにするためです。

C++では、constなオブジェクトに対しては、constメンバ関数以外を呼び出すことができません。

また、関数の引数として「オブジェクトを書き換えない参照(const T&)」を渡すことが一般的ですが、この受け取った側でメンバ関数を呼び出す際にも、その関数がconstである必要があります。

これにより、プログラム全体で「どこでデータが変更されるか」を厳密に制御できるようになります。

constメンバ関数の基本的な使い方

実際のコードでどのようにconstメンバ関数が機能するのか、具体的な例を見ていきましょう。

定義の方法とコンパイルチェック

constメンバ関数内では、メンバ変数を書き換えようとするとコンパイラがエラーを出してくれます。

これは、プログラマのうっかりミスを防ぐ強力なガードになります。

C++
#include <iostream>
#include <string>

class Circle {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    // 面積を計算する(半径は変えないのでconst)
    double getArea() const {
        return radius * radius * 3.14159;
    }

    // 半径を変更する(状態が変わるのでconstにはできない)
    void setRadius(double r) {
        radius = r;
    }
};

int main() {
    const Circle c1(5.0); // constなオブジェクト

    std::cout << "Area: " << c1.getArea() << std::endl;

    // c1.setRadius(10.0); // エラー:constオブジェクトから非const関数は呼べない

    return 0;
}
実行結果
Area: 78.5397

上記の例では、c1はconstとして宣言されているため、getArea()は呼び出せますが、setRadius()を呼び出そうとするとコンパイル時に拒否されます。

これにより、不変であるべきデータが不意に書き換わるリスクをゼロにできます。

プロトタイプ宣言と定義を分ける場合

クラスの定義(ヘッダファイル)と実装(cppファイル)を分ける場合、両方の場所にconstを記述する必要がある点に注意してください。

C++
// ヘッダファイル (.h)
class MyClass {
public:
    void display() const; // ここにconst
};

// 実装ファイル (.cpp)
void MyClass::display() const { // ここにもconstが必要
    std::cout << "Hello" << std::endl;
}

片方に書き忘れると、コンパイラは「constあり」と「constなし」の別の関数であると判断してしまい、リンクエラーの原因となります。

mutableキーワードの活用

原則としてconstメンバ関数内ではメンバ変数を変更できませんが、実務では「オブジェクトの論理的な状態は変わらないが、内部的なキャッシュや統計情報だけは更新したい」という場面があります。

これを実現するのがmutableキーワードです。

論理的な定数性と物理的な定数性

C++には「物理的な定数性(ビットレベルで変更しない)」と「論理的な定数性(ユーザーから見て状態が変わっていない)」の2つの考え方があります。

用語意味
物理的な定数性メモリ上のデータが1ビットも変わらないこと。
論理的な定数性オブジェクトの主要な値や振る舞いは変わらないが、内部的な細部は変わっても良いこと。

mutableを使用すると、物理的な定数性を一部崩して、論理的な定数性を維持したまま、特定の変数を変更できるようになります。

キャッシュ実装での活用例

例えば、計算コストの高い関数の結果を一度だけ計算して保存しておく「メモ化」や、関数の呼び出し回数をカウントするようなケースでmutableが活躍します。

C++
#include <iostream>

class DataProcessor {
private:
    mutable int accessCount = 0; // mutableにすることでconst関数内でも変更可能
    int data = 100;

public:
    // データを取得する。論理的には状態を変えないが、アクセス回数は記録したい。
    int getData() const {
        accessCount++; // const関数内だが、mutableなので変更できる
        return data;
    }

    int getAccessCount() const {
        return accessCount;
    }
};

int main() {
    const DataProcessor dp;
    
    std::cout << "Data: " << dp.getData() << std::endl;
    std::cout << "Data: " << dp.getData() << std::endl;
    std::cout << "Access Count: " << dp.getAccessCount() << std::endl;

    return 0;
}
実行結果
Data: 100
Data: 100
Access Count: 2

このように、ユーザーから見ればデータを取り出しているだけ(getData)ですが、内部的にアクセス統計を取るような処理が自然に記述できます。

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

C++では、constの有無だけが異なる同名のメンバ関数を定義することができます。

これを「constによるオーバーロード」と呼びます。

戻り値の型を変えるテクニック

よく使われるのは、配列のようなクラスで、要素への参照を返す場合です。

読み取り専用の時はconstな参照を返し、書き込み可能な時は通常の参照を返すように設計します。

C++
#include <iostream>
#include <vector>

class MyArray {
private:
    std::vector<int> vec = {1, 2, 3, 4, 5};

public:
    // 非const版:要素の書き換えを許可する
    int& operator[](size_t index) {
        std::cout << "(non-const version)" << std::endl;
        return vec[index];
    }

    // const版:要素の読み取りのみ許可する
    const int& operator[](size_t index) const {
        std::cout << "(const version)" << std::endl;
        return vec[index];
    }
};

int main() {
    MyArray a;
    const MyArray ca;

    a[0] = 10;           // 非const版が呼ばれる
    int val = ca[0];     // const版が呼ばれる
    // ca[0] = 20;       // エラー:const版はconst int&を返すため代入不可

    return 0;
}
実行結果
(non-const version)
(const version)

この仕組みがあるおかげで、標準ライブラリのstd::vectorstd::stringは、constオブジェクトであっても効率的に要素へアクセスできるようになっています。

注意点とベストプラクティス

constメンバ関数を使いこなす上で、いくつか落とし穴があります。

特にポインタを扱う場合には注意が必要です。

メンバ変数のポインタ先は変更できてしまう問題

constメンバ関数が守るのは「メンバ変数そのもの」です。

メンバ変数がポインタである場合、ポインタが指している先の中身の変更は、C++の言語仕様上、constメンバ関数内でも制限されません。

C++
class PointerExample {
private:
    int* ptr;

public:
    PointerExample(int* p) : ptr(p) {}

    void illegalModify() const {
        *ptr = 999; // コンパイルが通ってしまう!
    }
};

これは「ビットレベルの定数性」を守っているものの、論理的には定数性を破壊している状態です。

このような設計はバグの温床となるため、指し先も変更しないように実装者が配慮することが求められます。

const_castの使用を避ける

const_castを使用すると、強制的にconst性を外して値を書き換えることができます。

しかし、これは最終手段であり、通常のアプリケーション開発で使用すべきではありません。

特に、元々constとして定義されたオブジェクトに対してconst_castで書き込みを行うと、未定義動作(プログラムがクラッシュしたり、予期せぬ挙動をしたりする)を引き起こす可能性があります。

ベストプラクティス:可能な限りconstにする

クラスを設計する際は、「状態を変えない関数はすべてconstにする」というのが鉄則です。

これを「constの正確性(const correctness)」と呼びます。

最初からconstを意識して設計することで、後から「constオブジェクトからこの関数が呼べない!」と困る事態を防ぐことができます。

まとめ

C++のconstメンバ関数は、単なる制約ではなく、クラスの利用者に対して「この操作は安全である」という明確な契約を示すためのツールです。

  • 基本:関数の後ろにconstを付け、メンバ変数の変更を禁止する。
  • 重要性:constオブジェクトやconst参照から呼び出すために必須。
  • 拡張:mutableを使えば、論理的な一貫性を保ちつつ一部の変数のみ変更可能。
  • 設計:constによるオーバーロードを活用し、読み取りと書き込みで最適なインターフェースを提供する。

これらを正しく理解し活用することで、バグが少なく、意図の伝わりやすい高品質なC++コードを記述できるようになります。

常に「この関数は状態を変える必要があるか?」を自問自答し、可能な限りconstを付与する習慣をつけましょう。

クラスの定義と基本

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!