C++のmutableとは
C++のmutableは、constなオブジェクトやconstメンバ関数の中でも、特定のデータメンバだけ変更できるようにするためのキーワードです。
通常、constメンバ関数ではオブジェクトのメンバ変数を書き換えることはできません。
しかし、メンバ変数にmutableを指定すると、そのメンバについてはconstオブジェクトの内部でも変更可能になります。
基本的な書き方は次のとおりです。
class Sample {
private:
mutable int value;
};
この場合、valueはmutableな非staticデータメンバです。
そのため、Sampleオブジェクト自体がconstであっても、valueは変更できます。
mutableは単純に「constを無効化するキーワード」ではありません。
主に、オブジェクトの論理的な状態を変えずに、キャッシュやアクセス回数、同期用オブジェクトなどの内部状態だけを更新したい場合に使用します。
mutableとconstメンバ関数の関係
constメンバ関数では通常メンバを変更できない
C++では、メンバ関数の末尾にconstを付けると、その関数がオブジェクトの状態を変更しないことを表現できます。
例えば、次のコードです。
class Counter {
private:
int count = 0;
public:
int getCount() const {
return count;
}
};
次の部分がconstメンバ関数であることを表しています。
int getCount() const
constメンバ関数では、通常の非staticデータメンバを書き換えることはできません。
例えば、次のコードはコンパイルエラーになります。
class Counter {
private:
int count = 0;
public:
int getCount() const {
++count; // エラー
return count;
}
};
getCount()がconstであるため、通常のメンバ変数であるcountを変更できないからです。
mutableを付けると変更できる
countにmutableを指定すると、constメンバ関数の中でも変更できるようになります。
class Counter {
private:
mutable int count = 0;
public:
int getCount() const {
++count;
return count;
}
};
この場合、getCount()はconstのままですが、countについては変更できます。
つまり、mutableは、
「この非staticデータメンバについては、包含するオブジェクトがconstであっても変更を許可する」
ための指定です。
mutableの基本的な使い方
アクセス回数を記録する例
例えば、値そのものは変更せず、何回アクセスされたかだけを記録したい場合があります。
#include <iostream>
class Data {
private:
int value;
mutable int accessCount = 0;
public:
Data(int v) : value(v) {}
int getValue() const {
++accessCount;
return value;
}
int getAccessCount() const {
return accessCount;
}
};
int main() {
const Data data(100);
std::cout << data.getValue() << '\n';
std::cout << data.getValue() << '\n';
std::cout << data.getAccessCount() << '\n';
}
実行結果は次のようになります。
100
100
2
dataは次のようにconstとして宣言されています。
const Data data(100);
そのため、通常のデータメンバは変更できません。
一方、accessCountには、
mutable int accessCount = 0;
とmutableが指定されています。
そのため、constメンバ関数であるgetValue()の中でも、
++accessCount;
と変更できます。
constオブジェクトでもmutableメンバは変更できる
通常のメンバとmutableメンバの違い
次のクラスを考えてみます。
class Test {
public:
int normal = 10;
mutable int special = 20;
};
このクラスからconstオブジェクトを作成します。
const Test obj;
通常のメンバであるnormalは変更できません。
obj.normal = 100; // エラー
一方、mutableを指定したspecialは変更できます。
obj.special = 200; // OK
全体では次のようになります。
#include <iostream>
class Test {
public:
int normal = 10;
mutable int special = 20;
};
int main() {
const Test obj;
// obj.normal = 100; // エラー
obj.special = 200;
std::cout << obj.special << '\n';
}
実行結果は次のとおりです。
200
このように、mutableな非staticデータメンバは、包含するオブジェクトがconstであっても変更可能です。
mutableの代表的な用途
キャッシュを保存する
mutableの代表的な用途の一つがキャッシュです。
例えば、計算コストが高い処理を毎回実行するのではなく、一度計算した結果を保存して再利用したい場合があります。
#include <iostream>
class Calculator {
private:
int value;
mutable bool cached = false;
mutable int cache = 0;
public:
Calculator(int v) : value(v) {}
int calculate() const {
if (!cached) {
cache = value * value;
cached = true;
}
return cache;
}
};
int main() {
const Calculator calc(10);
std::cout << calc.calculate() << '\n';
std::cout << calc.calculate() << '\n';
}
calculate()は外部から見ると、計算結果を取得するだけの処理です。
そのため、
int calculate() const
とするのが自然です。
しかし、内部では、
cache = value * value;
cached = true;
のようにキャッシュ情報を更新しています。
そこで、
mutable bool cached;
mutable int cache;
と指定します。
これにより、外部に対するconst性を維持しながら、内部的なキャッシュだけを更新できます。
アクセス回数や統計情報を保存する
mutableは、アクセス回数や統計情報の記録にも利用できます。
例えば次のようなコードです。
#include <string>
#include <utility>
class User {
private:
std::string name;
mutable int accessCount = 0;
public:
User(std::string n)
: name(std::move(n)) {}
const std::string& getName() const {
++accessCount;
return name;
}
int getAccessCount() const {
return accessCount;
}
};
getName()はユーザー名そのものを変更しません。
一方で、内部では何回アクセスされたかを記録しています。
このような情報は、オブジェクトが本質的に表す状態とは別の管理情報として扱えるため、mutableが適しています。
mutexをmutableにする
実務的なC++コードでは、std::mutexをmutableにするケースもあります。
#include <mutex>
class Data {
private:
int value = 0;
mutable std::mutex mutex;
public:
int getValue() const {
std::lock_guard<std::mutex> lock(mutex);
return value;
}
};
getValue()は値を読み取るだけなので、インターフェース上はconstにするのが自然です。
しかし、安全に値を読み取るためにはmutexをロックする必要があります。
mutexのロック操作はmutex自身の内部状態を変更します。
そのため、
mutable std::mutex mutex;
と宣言することで、constメンバ関数からでも同期処理を行えるようにします。
ただし、mutableを指定するだけでクラスが自動的にスレッドセーフになるわけではありません。
どのデータをどのmutexで保護するかを適切に設計する必要があります。
mutableと論理的constの関係
物理的constとは
mutableを理解するうえで重要なのが、「物理的const」と「論理的const」という考え方です。
物理的constは、オブジェクト内部のデータを物理的に変更しないという考え方です。
例えば、
const Data data;
とすると、通常のデータメンバは変更できません。
論理的constとは
一方、論理的constは、
外部から見たオブジェクトの意味や状態が変わらなければ、内部実装上の変更は許容する
という考え方です。
例えば、次の関数があるとします。
int calculate() const;
利用者から見ると、この関数は単に計算結果を取得しているだけです。
内部でキャッシュを更新したとしても、Calculatorが表している本質的な値は変わりません。
このような内部変更を実現するためにmutableが利用されます。
したがって、mutableは、
「constを無理に回避するための仕組み」ではなく、「論理的constを実現するための仕組み」
として理解すると分かりやすいでしょう。
mutableとconst_castの違い
const_castでもconstを外せる場合がある
C++にはconst_castというキャストがあります。
例えば、
const_cast<Sample*>(this)->value = 100;
のようなコードを書くこともできます。
しかし、キャッシュや管理情報を変更したいだけであれば、通常はmutableを使用した方が意図が明確です。
mutable int cache;
とクラス定義に書かれていれば、
「このメンバはconstオブジェクトの内部でも変更される可能性がある」
ことが明確になります。
const_castには未定義動作の危険がある
特に注意したいのは、本当にconstとして定義されたオブジェクトをconst_castで書き換えることです。
例えば、
const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;
のように、もともとconstとして定義されたオブジェクトを変更すると、未定義動作になります。
そのため、論理的constを実装する目的であれば、安易にconst_castを使うのではなく、設計上変更可能なメンバにmutableを指定する方が適切です。
mutableを使うときの注意点
オブジェクトの本質的な状態には安易に使わない
mutableを指定するとconstオブジェクト内でも変更できるため、便利だからといって何にでも付けるべきではありません。
例えば、
class Account {
private:
mutable int balance;
};
のように、銀行口座の残高そのものをmutableにする設計は通常適切ではありません。
残高はAccountオブジェクトの本質的な状態だからです。
例えば、
void something() const {
balance -= 100;
}
のような変更が可能になると、「この関数はオブジェクトの論理的状態を変更しない」というconstの意味が崩れてしまいます。
mutableは主に、
- キャッシュ
- アクセス回数
- 統計情報
- デバッグ情報
- ログ関連情報
- mutexなどの同期用オブジェクト
といった、オブジェクトの本質的な状態とは別の内部情報に利用するのが適しています。
mutableだけではスレッドセーフにならない
例えば、
mutable int count = 0;
というメンバがあったとしても、複数のスレッドから、
++count;
を同時に実行すれば、データ競合が発生する可能性があります。
mutableはあくまでconstとの関係を制御するキーワードです。
スレッドセーフ性を保証する機能ではありません。
必要に応じて、
std::mutex
や、
std::atomic
などを利用する必要があります。
キャッシュ用途の場合も同様で、複数スレッドから同時にキャッシュを初期化する可能性があるなら、別途同期処理を検討する必要があります。
mutableとstaticの違い
mutableはconstとの関係を指定する
mutableとstaticはまったく異なる役割を持っています。
例えば、
mutable int count;
とした場合、countは各オブジェクトごとに存在します。
Sample a;
Sample b;
なら、aとbはそれぞれ別々のcountを持ちます。
mutableは、そのメンバがconstオブジェクト内でも変更可能であることを指定します。
staticデータメンバはクラス全体で共有される
一方、
static int count;
とした場合、そのデータメンバはクラス全体で共有されます。
データメンバについて比較すると、役割は次のように整理できます。
mutable
→ constオブジェクト内での変更可能性に関係する
static
→ クラスの全オブジェクトで共有されるデータにする
なお、mutableは非staticデータメンバに指定するものであり、次のようにstaticデータメンバへ指定することはできません。
class Sample {
private:
mutable static int value; // エラー
};
ラムダ式でもmutableを使用できる
通常の値キャプチャは変更できない
C++では、ラムダ式にもmutableというキーワードが登場します。
例えば、
int x = 10;
auto func = [x]() {
// ++x; // 通常はエラー
};
[x]はxを値としてキャプチャしています。
通常、このように値キャプチャされたオブジェクトはラムダ内部から変更できません。
mutableラムダなら変更できる
ラムダにmutableを指定すると、値キャプチャしたコピーを変更できます。
#include <iostream>
int main() {
int x = 10;
auto func = [x]() mutable {
++x;
std::cout << x << '\n';
};
func();
func();
std::cout << x << '\n';
}
典型的な実行結果は次のようになります。
11
12
10
ラムダ内部に保存されたxのコピーは、
10 → 11 → 12
と変化しています。
一方、ラムダの外側にある元のxは変更されていません。
そのため、最後には、
10
と表示されます。
ラムダのmutableはメンバ変数のmutableとは用途が異なる
同じmutableというキーワードですが、データメンバとラムダ式では用途が異なります。
| 使用場所 | 主な意味 |
|---|---|
| 非staticデータメンバ | constオブジェクト内でもそのメンバを変更可能にする |
| ラムダ式 | 値キャプチャしたコピーをラムダ内部で変更可能にする |
クラスでは、
mutable int cache;
のように使用します。
ラムダでは、
[x]() mutable {
++x;
}
のように使用します。
この2つを区別して覚えることが重要です。
C++23以降のラムダでのmutableの注意点
一部の指定とは組み合わせられない
C++23以降ではラムダ式の機能が拡張されています。
そのため、mutableにもいくつか組み合わせ上の制約があります。
例えば、ラムダのstatic指定とmutableを同時に指定することはできません。
また、明示的オブジェクトパラメータを持つラムダではmutableを使用できません。
通常の、
[x]() mutable {
++x;
}
という使い方を理解するうえでは必須の知識ではありませんが、最新のC++仕様まで詳しく扱う場合には覚えておくとよいでしょう。
mutableを使った実用的なキャッシュの例
文字列の情報をキャッシュする
例えば、ある計算結果を一度だけ求めて保存しておくクラスを考えます。
#include <iostream>
#include <string>
#include <utility>
class Text {
private:
std::string text;
mutable bool cacheValid = false;
mutable std::size_t cachedLength = 0;
public:
explicit Text(std::string value)
: text(std::move(value)) {}
std::size_t length() const {
if (!cacheValid) {
cachedLength = text.length();
cacheValid = true;
}
return cachedLength;
}
};
int main() {
const Text text("Hello, C++");
std::cout << text.length() << '\n';
std::cout << text.length() << '\n';
}
利用者から見ると、
text.length();
は単に長さを取得しているだけです。
そのため、
std::size_t length() const
とするのが自然です。
一方、内部では、
cachedLength
cacheValid
を更新しています。
そこで、
mutable bool cacheValid;
mutable std::size_t cachedLength;
とすることで、constメンバ関数のままキャッシュを利用できます。
なお、このようなキャッシュ処理を複数スレッドから同時に呼び出す可能性がある場合は、データ競合を防ぐためにmutexなどによる同期も検討する必要があります。
C++のmutableを使う判断基準
mutableが適しているケース
mutableを使うか迷った場合は、
「この値が変わっても、外部から見たオブジェクトの意味は変わらないか」
を考えると判断しやすくなります。
例えば、
キャッシュが更新される
→ 本質的な値は変わらない
アクセス回数が増える
→ 本質的な値は変わらない
mutexがロックされる
→ 本質的な値は変わらない
と考えられるため、mutableを使う理由があります。
一方、
口座残高が変わる
ユーザー名が変わる
商品の価格が変わる
座標が変わる
といった変更は、通常オブジェクトの論理的な状態そのものを変えます。
このようなメンバをmutableにする場合は、設計を慎重に検討した方がよいでしょう。
C++のmutableの重要ポイント
C++のmutableについて、重要なポイントを整理すると次のとおりです。
mutableは主に非staticデータメンバに指定するキーワードです。mutableなデータメンバは、包含するオブジェクトがconstでも変更できます。constメンバ関数の中からもmutableメンバを変更できます。- キャッシュ、アクセス回数、統計情報、mutexなどに利用されます。
- オブジェクトの本質的な状態を表すメンバには安易に使用しない方がよいです。
mutable自体にはスレッドセーフを保証する機能はありません。const_castとは目的が異なり、論理的constを表現する場合はmutableの方が意図を明確にできます。staticデータメンバにはmutableを指定できません。- ラムダ式の
mutableは、値キャプチャしたコピーをラムダ内部で変更可能にする機能です。 - クラスのデータメンバに付ける
mutableとラムダ式のmutableは、同じキーワードでも用途が異なります。
mutableを理解するときに最も重要なのは、単なる「const制約を回避する機能」と考えないことです。
外部から見たオブジェクトの論理的な状態は変えず、内部実装に必要な状態だけを変更するための仕組み
と考えると、C++におけるmutableの役割を正しく理解しやすくなります。
以上、C++のmutableの意味や使い方についてでした。
最後までお読みいただき、ありがとうございました。
