第25回 メンバ関数と const メンバ
クラスの中に書く関数がメンバ関数。「このクラスができること」を表現します。本回は定義の書き方 3 パターン、const を関数末尾に付ける意味、inline の実体、そして「読むだけ関数」には必ず const を付ける習慣を身につけます。
このページで押さえること
✅ 最低限ここだけ覚える
- メンバ関数は
obj.method() で呼ぶ
- メンバを書き換えないなら関数末尾に
const
- クラス内で定義するとヘッダ配置になる
const Foo& からは非 const 関数を呼べない
⭐ 余裕があれば読む
- クラス内定義は暗黙的に
inline
- const オーバーロード(同名で const/非 const)
mutable で const を回避
- 宣言と定義の分離(ヘッダと .cpp)
1. まず触ってみる ― Rectangle
「面積」「幅と高さのリサイズ」ができる長方形クラスを作ってみます。
最小コード
rect.cpp最小例
class Rectangle {
int w_, h_;
public:
Rectangle(int w, int h) : w_(w), h_(h) {} // 構造は次回
int area() const { return w_ * h_; } // const: 読むだけ
void resize(int w, int h) { w_ = w; h_ = h; } // 書き換えるので const なし
};
Rectangle r{5, 3};
std::cout << r.area(); // 15
r.resize(4, 6);
std::cout << r.area(); // 24
ここまでで覚えること(2 つ):
- クラス内の関数はメンバ関数。
obj.method() で呼ぶ
- メンバを変更しない関数の末尾には
const を付ける(int area() const)
よくある素朴な疑問
Q. 関数の末尾の const は関数のどこに効いてる?
→ 「この関数は自分(this)が指すオブジェクトを変更しない」という宣言です。コンパイラが中で w_ = ... のような書き換えを弾いてくれます。
Q. なぜ data をクラス内定義 / 外定義で書き分けるの?
→ クラス内定義はヘッダに入る(全利用者が見る)のでコンパイル時間に影響。.cpp 側に定義を出すと実装の変更で再コンパイルが最小限で済む。§2 で解説。
Q. Rectangle r{5, 3}; の中カッコって何?
→ 第 9 回で扱った統一初期化。コンストラクタ(第 30 回)の引数をここで渡しています。r(5, 3) でも同じ意味。
2. 定義の書き方 3 パターン
① クラス内で完結させる
rect.h全部ヘッダ
class Rectangle {
int w_, h_;
public:
int area() const { return w_ * h_; } // 定義まで書く
void resize(int w, int h) { w_ = w; h_ = h; }
};
小さい関数・テンプレート・短い実装向き。クラス内で定義すると自動的に inline(複数の翻訳単位で同じ定義を持っても OK)になります。
② 宣言のみクラス内、定義は外
rect.h(宣言)ヘッダ
class Rectangle {
int w_, h_;
public:
int area() const;
void resize(int w, int h);
};
rect.cpp(定義)実装
#include "rect.h"
int Rectangle::area() const { // クラス名:: を書く
return w_ * h_;
}
void Rectangle::resize(int w, int h) {
w_ = w; h_ = h;
}
大規模なプロジェクトで標準的。実装の変更がヘッダに及ばないので、利用側の再コンパイルが減ります。関数名の前に Rectangle:: を忘れずに。
③ 部分的に外出し(よくあるパターン)
小さい関数(getter など)はクラス内、重い関数は .cpp に、と使い分けます。
混在実用的
class Rectangle {
int w_, h_;
public:
// シンプルな getter はクラス内で(inline)
int width() const { return w_; }
int height() const { return h_; }
// 重い処理は .cpp 側で
void draw() const;
};
判断目安:
- 1〜2 行の getter/アクセサ → クラス内定義
- 数十行の処理/ログ・I/O を含む → .cpp 側
- テンプレートメンバ関数 → ヘッダ側必須
3. const メンバ関数
関数の末尾に const を付けると「この関数はメンバ変数を変更しません」という約束になります。
const あり読むだけ
class Point {
int x_, y_;
public:
int get_x() const {
return x_; // 読むのはOK
}
int bad() const {
x_ = 0; // コンパイルエラー
return x_;
}
};
const なし書き換え可
class Point {
int x_, y_;
public:
void set_x(int x) { // const なし
x_ = x; // 書き換え OK
}
};
ルール:「読むだけ」には必ず const を付ける
getter や問い合わせ系の関数(size(), empty(), area(), is_ready() など)は必ず const を付けるのが現代 C++ の流儀です。なぜ重要かは次の §4 で。
よくあるミス: 宣言側(クラス内)にだけ const を付けて、定義側に付け忘れ。
int area() const;(ヘッダ)と書いたら、int Rectangle::area() const { ... }(.cpp)と両方に const が必要。片方だけだと「別の関数」と見なされ、リンクエラーに。
4. const オブジェクトから見える世界
const 化したオブジェクトからは、const メンバ関数しか呼べません。これが const の真価。
const Point から見える関数読取専用
class Point {
int x_, y_;
public:
int get_x() const; // const あり
void set_x(int); // const なし
};
const Point cp{3, 4};
cp.get_x(); // OK
cp.set_x(5); // コンパイルエラー
🔒 const オブジェクト const Point cp から呼べる/呼べない
✅cp.get_x()const メンバ関数なので OK
✅cp.get_y()const メンバ関数なので OK
❌cp.set_x(10)const なしの関数は呼べない
❌cp.x_ = 10private なので論外、仮に public でも const
なぜ重要か ― 関数引数の const Foo&
関数の引数で const std::string& などをよく使うのを思い出してください(第 10 回)。その関数の中では const メンバ関数しか呼べないので、「読み取り関数に const を付けてないクラス」は引数に渡せなくなります。
const 忘れの被害使いにくい
class User {
std::string name_;
public:
std::string name() { // const 忘れ
return name_;
}
};
void greet(const User& u) {
std::cout << u.name(); // エラー
}
正しい書き方const 付き
class User {
std::string name_;
public:
std::string name() const { // const あり
return name_;
}
};
void greet(const User& u) {
std::cout << u.name(); // OK
}
ルール: 「書き換えない関数には必ず const を付ける」。これを徹底するだけで、利用者が安心して使える堅牢なクラスになります。
⭐
ここまでで日常は OK
残りは mutable と const オーバーロード。応用的な話ですが、キャッシュ実装で役立ちます。
5. mutable と const オーバーロード
mutable ― const 関数内でも書き換えたいメンバ
「オブジェクトの論理的な状態は変えていないが、キャッシュやロックなどの実装詳細は変えたい」場合に使います。
キャッシュ例mutable
class Expensive {
mutable int cache_ = -1; // const 関数で書ける
public:
int value() const {
if (cache_ == -1)
cache_ = compute(); // mutable なので OK
return cache_;
}
};
const オーバーロード ― 同じ名前で const/非 const 版
STL の std::vector などでよく使われるテクニック。
vector の at()stdlib
class MyVec {
int* data_;
public:
// 非 const: 書き換え可能な参照を返す
int& at(size_t i) { return data_[i]; }
// const: 読み取り専用の参照を返す
const int& at(size_t i) const { return data_[i]; }
};
MyVec v; v.at(0) = 99; // 非 const 版が呼ばれる
const MyVec cv; auto x = cv.at(0); // const 版が呼ばれる
mutable の乱用に注意: mutable は「外から見た論理的な不変性は保たれている」場合の抜け穴。キャッシュ・ロック・統計カウンタなどに限るのが流儀。「面倒だから mutable」はアンチパターン。
確認クイズ
メンバ関数と const を 4 問で確認。
Q1. メンバ関数末尾の const の意味は?
戻り値が const になる
引数が const になる
この関数内でメンバ変数を書き換えない宣言
この関数がスレッドセーフになる
関数末尾の const は「this が指すオブジェクトを変更しない」ことを宣言します。コンパイラが中でメンバ変数への書き換えを弾きます。戻り値の const は関数名の前(const int foo())、引数の const は引数の型に付けます。
Q2. const User& u から u.set_name("Bob")(非 const メンバ関数)を呼ぶとどうなる?
OK だが警告
コンパイルエラー
実行時エラー
名前は変わらないが通る
const オブジェクト(const 参照も同じ)からは、const メンバ関数しか呼べません。set 系の非 const 関数を呼ぼうとするとコンパイルエラー。これが「読むだけの関数には必ず const を付ける」べき理由。
Q3. クラス内にメンバ関数を定義すると暗黙的にどうなる?
const になる
virtual になる
inline になる
friend になる
クラス内で定義したメンバ関数は暗黙的に inline。これにより複数の翻訳単位で同じ定義を持っても One Definition Rule 違反にならず、ヘッダに書けます。
Q4. mutable の用途は?
変数の型を実行時に変える
const メンバ関数の中でもそのメンバは書き換え可能にする
private メンバを public のように扱う
継承で base クラスから変更可能にする
mutable は「論理的には不変だが実装上書き換える必要があるメンバ」向け。キャッシュ、計測カウンタ、ロックオブジェクトなどに使います。普通のメンバ変数には付けるべきではありません(const の意味が弱まる)。