C++入門|const修飾子

変えてよい力と、守るべき力を明確にする。constは、戦士の重要なデータを誤操作から守る変更禁止の封印です。

C++で複数のクラスや関数が協力するプログラムを作るようになると、同じオブジェクトをさまざまな場所から利用するようになります。

たとえば、戦士の情報を管理するクラスを、修行装置、戦闘力測定器、回復装置などへ渡す場面を考えてみましょう。

戦闘力測定器は、戦士の現在の状態を読み取るだけで十分です。ところが、渡されたオブジェクトを自由に変更できる設計になっていると、測定するだけのはずの処理が、戦士の気力や技名を書き換えてしまう可能性があります。

そこで役立つのがconst修飾子です。

constは、値やオブジェクトに対して、ここでは変更してはいけないという制限を付けるために使います。

ドラゴンボール風にたとえるなら、constは神殿の重要な修行記録にかける保護結界です。

記録を読むことはできますが、勝手に書き換えることはできません。変更しようとすると、コンパイラが危険を知らせて処理を止めてくれます。

constは、主に次のような場所で使われます。

constを使う場所主な役割
変数初期化後の値を変更できないようにする
ポインタや参照の引数関数内から対象のオブジェクトを変更できないようにする
メンバ関数の末尾その関数内でオブジェクトの状態を変更できないようにする
staticメンバ変数クラス全体で共有する変更不可能な値を定義する

constを正しく使うと、プログラムを読む人にも、コンパイラにも、どのデータを変更してよいのかを明確に伝えられます。

const修飾子とは

constは、constantをもとにしたキーワードです。

変数の前にconstを付けると、その変数は初期化後に変更できなくなります。

const int MAX_ENERGY = 100;

このMAX_ENERGYには100が設定されています。

初期化後に、別の値を代入することはできません。

MAX_ENERGY = 120;

この処理はコンパイルエラーになります。

MAX_ENERGYは、最大気力というプログラム全体で意味を持つ値です。途中で120や150へ変わってしまうと、気力計算のルールが崩れてしまいます。

そこでconstを付け、変更できない値として管理します。

図:constは値を守る変更禁止の封印

この図が示していること

この図では、constを付けた値が保護されていることを表しています。

MAX_ENERGYの値を読み取ることはできます。しかし、初期化後に別の値を代入することはできません。

変更しようとした時点でコンパイラがエラーを出すため、プログラムを実行する前に誤りを発見できます。

constを付けた変数

通常の変数は、プログラムの途中で値を変更できます。

int energy = 50;
energy = 80;

energyは通常の変数なので、50から80へ変更できます。

一方、constを付けた変数は変更できません。

const int MAX_ENERGY = 100;

次の代入は許可されません。

MAX_ENERGY = 120;

const変数は、宣言するときに初期値を与えるのが基本です。

const int MAX_ENERGY = 100;

次のように、初期値を与えずに宣言することはできません。

const int MAX_ENERGY;

あとから値を設定できないため、宣言時に値が決まっていなければならないからです。

変数あとから変更できるか用途
int energy = 50できる現在の気力など、変化する値
const int MAX_ENERGY = 100できない最大値など、変更させたくない値

ドラゴンボール風に考えると、energyは修行によって増減する現在の気力です。

MAX_ENERGYは神殿が定めた最大気力の上限です。現在の気力は変化しますが、最大値の規則は勝手に変更してはいけません。

constを使うと意図が伝わりやすくなる

次の変数を見てみましょう。

int maxEnergy = 100;

変数名から最大気力を表していることは分かりますが、プログラム上は変更できます。

一方、次のように書くと、変更してはいけない値であることが明確になります。

const int MAX_ENERGY = 100;

constを付けることで、プログラムを読む人にも、この値は途中で変更しないという設計上の意図を伝えられます。

コンパイラも変更を禁止してくれるため、コメントで注意を促すだけよりも安全です。

関数の引数をconst指定する

constは、関数の引数にも付けられます。

特に大切なのが、ポインタや参照を使ってオブジェクトを渡す場合です。

通常の参照でオブジェクトを受け取ると、関数内から呼び出し元のオブジェクトを変更できます。

void changeEnergy(WarriorProfile& profile);

一方、const参照で受け取ると、関数内からオブジェクトを変更できなくなります。

void showProfile(const WarriorProfile& profile);

このprofileは、読み取り専用として扱われます。

オブジェクト全体をコピーせずに受け取れますが、そのオブジェクトの状態を変更する処理は行えません。

const参照

クラスのオブジェクトを読み取り専用で受け取りたい場合は、const参照がよく使われます。

void showProfile(const WarriorProfile& profile);

この宣言を分解すると、次のようになります。

部分意味
const対象のオブジェクトを変更しない
WarriorProfile受け取るクラスの型
&参照として受け取る
profile仮引数名

参照で受け取るため、WarriorProfileオブジェクト全体のコピーを作る必要がありません。

さらにconstが付いているため、関数内から元のオブジェクトを書き換えることもできません。

つまり、const参照には次の2つの役割があります。

役割内容
コピーを避ける大きなオブジェクトを効率よく受け渡せる
変更を禁止する関数内から呼び出し元の状態を変更できない

constポインタ

ポインタを使って読み取り専用のオブジェクトを受け取ることもできます。

void inspectProfile(const WarriorProfile* profile);

この場合、profileが指しているWarriorProfileオブジェクトを、関数内から変更できません。

profile->showStatus();

状態を表示するようなconstメンバ関数は呼び出せます。

一方、状態を変更するメンバ関数は呼び出せません。

profile->setEnergy(100);

setEnergyが通常のメンバ関数であれば、const WarriorProfile*を通して呼び出すことはできません。

const参照とconstポインタの基本的な違いは次のとおりです。

比較項目const参照constポインタ
宣言const WarriorProfile& profileconst WarriorProfile* profile
呼び出しshowProfile(profile)inspectProfile(&profile)
メンバアクセスprofile.showStatus()profile->showStatus()
nullの可能性基本的に対象が必要nullptrの可能性がある
主な使い方必ず存在するオブジェクトを読む対象が存在しない可能性も表現する

値渡しの引数に付けたconst

値渡しの引数にもconstを付けられます。

void setLevel(const int level);

この場合、関数内でlevelを変更できません。

void setLevel(const int level) {
    level = 10;
}

この代入はコンパイルエラーになります。

ただし、値渡しでは最初から引数のコピーが作られます。関数内で引数を書き換えても、呼び出し元の変数には影響しません。

そのため、intのような小さな型では、次のようにconstを付けずに受け取ることも一般的です。

void setLevel(int level);

一方、stringやvector、自作クラスなどを読み取り専用で受け取る場合は、const参照がよく使われます。

void setTechniqueName(const string& techniqueName);

この書き方なら、文字列全体のコピーを避けながら、関数内で引数を書き換えないようにできます。

constメンバ関数

クラスのメンバ関数にもconstを付けられます。

メンバ関数をconst指定する場合は、引数リストの後ろにconstを書きます。

int getEnergy() const;

このconstは、getEnergyの中でオブジェクトの状態を変更しないことを表します。

たとえば、次の関数は気力を返すだけです。

int WarriorProfile::getEnergy() const {
    return m_energy;
}

m_energyを読み取っていますが、変更はしていません。

そのため、constメンバ関数として定義できます。

一方、次の関数はm_energyを変更するため、constを付けられません。

void WarriorProfile::setEnergy(int energy) {
    m_energy = energy;
}
メンバ関数状態を変更するかconstを付けるか
getEnergy変更しない付ける
getTechniqueName変更しない付ける
showStatus変更しない付ける
setEnergy変更する付けない
setTechniqueName変更する付けない

図:const参照とconstメンバ関数の連携

この図が示していること

この図では、WarriorProfileオブジェクトをconst参照で関数へ渡す流れを表しています。

const参照を通してオブジェクトを利用する場合、getEnergyやshowStatusのようなconstメンバ関数は呼び出せます。

一方、setEnergyやsetTechniqueNameのようにオブジェクトの状態を変更するメンバ関数は呼び出せません。

const参照とconstメンバ関数を組み合わせることで、読み取り専用の安全な処理を作れます。

constを使ったクラスを作成する

ここでは、戦士の技名と現在の気力を管理するWarriorProfileクラスを作成します。

このクラスでは、次の場所でconstを使用します。

使用場所使用例
staticメンバ変数static const int MAX_ENERGY
setterの引数const string& techniqueName
getterstring getTechniqueName() const
状態取得int getEnergy() const
状態表示void showStatus() const
読み取り専用の関数引数const WarriorProfile& profile

WarriorProfileクラスのヘッダーファイル

プロジェクト/ファイル名: Chap7_03/warrior_profile.h

#ifndef CHAP7_03_WARRIOR_PROFILE_H
#define CHAP7_03_WARRIOR_PROFILE_H

#include <string>

// 戦士の技名と気力を管理するクラス
class WarriorProfile {
private:
    // 戦士が使用する技名
    std::string m_techniqueName;

    // 戦士の現在の気力
    int m_energy;

public:
    // クラス全体で共有する最大気力
    static const int MAX_ENERGY = 100;

    // コンストラクタ
    WarriorProfile();

    // 技名を設定する
    void setTechniqueName(const std::string& techniqueName);

    // 技名を取得する
    std::string getTechniqueName() const;

    // 気力を設定する
    void setEnergy(int energy);

    // 現在の気力を取得する
    int getEnergy() const;

    // 戦士の状態を表示する
    void showStatus() const;
};

#endif

ヘッダーファイルでは、MAX_ENERGYをstatic constメンバ変数として宣言しています。

static const int MAX_ENERGY = 100;

staticが付いているため、MAX_ENERGYは各オブジェクトが個別に持つ値ではありません。

WarriorProfileクラス全体に1つだけ存在する共有データです。

constも付いているため、値を変更できません。

static constメンバ変数

MAX_ENERGYにはstaticとconstの両方が付いています。

static const int MAX_ENERGY = 100;

それぞれの役割は次のとおりです。

修飾子役割
staticオブジェクトごとではなく、クラス全体で共有する
const初期化後に値を変更できないようにする

staticメンバはクラスに属するため、クラス名からアクセスする書き方が分かりやすいです。

WarriorProfile::MAX_ENERGY

オブジェクトを作らなくても利用できます。

cout << WarriorProfile::MAX_ENERGY << endl;

オブジェクトからアクセスすることもできますが、staticメンバであることが分かりやすいように、クラス名からアクセスするのが一般的です。

WarriorProfileクラスの処理を実装する

プロジェクト/ファイル名: Chap7_03/warrior_profile.cpp

#include "warrior_profile.h"
#include <iostream>

using namespace std;

// 古いC++規格でも利用できるように実体を定義する
const int WarriorProfile::MAX_ENERGY;

// コンストラクタ
WarriorProfile::WarriorProfile()
    : m_techniqueName("未設定"), m_energy(0) {
}

// 技名を設定する
void WarriorProfile::setTechniqueName(
    const string& techniqueName) {

    m_techniqueName = techniqueName;

    // const参照なので、次の変更はできない
    // techniqueName = "別の技";
}

// 技名を取得する
string WarriorProfile::getTechniqueName() const {
    // constメンバ関数なので、次の変更はできない
    // m_techniqueName = "変更された技";

    return m_techniqueName;
}

// 気力を設定する
void WarriorProfile::setEnergy(int energy) {
    // 0未満の値は0に修正する
    if (energy < 0) {
        m_energy = 0;
    }
    // 最大気力を超えた値はMAX_ENERGYに修正する
    else if (energy > MAX_ENERGY) {
        m_energy = MAX_ENERGY;
    }
    else {
        m_energy = energy;
    }
}

// 現在の気力を取得する
int WarriorProfile::getEnergy() const {
    return m_energy;
}

// 戦士の状態を表示する
void WarriorProfile::showStatus() const {
    cout << "技名:" << m_techniqueName << endl;
    cout << "現在気力:" << m_energy << endl;
    cout << "最大気力:" << MAX_ENERGY << endl;
}

const参照を使ったsetter

setTechniqueNameでは、引数をconst参照で受け取っています。

void WarriorProfile::setTechniqueName(
    const string& techniqueName)

stringを値渡しすると、関数へ渡すときに文字列のコピーが作られます。

void setTechniqueName(string techniqueName);

const参照なら、元の文字列を参照するため、文字列全体のコピーを避けられます。

void setTechniqueName(const string& techniqueName);

さらにconstが付いているため、関数内でtechniqueNameを書き換えることはできません。

techniqueName = "別の技";

この処理はコンパイルエラーになります。

ただし、引数の値をメンバ変数へコピーすることはできます。

m_techniqueName = techniqueName;

禁止されているのは、引数が参照している元の文字列を変更することです。

constメンバ関数を使ったgetter

getTechniqueNameの末尾にはconstが付いています。

string getTechniqueName() const;

関数定義にも同じようにconstを付けます。

string WarriorProfile::getTechniqueName() const {
    return m_techniqueName;
}

この関数は、m_techniqueNameを返すだけです。

オブジェクトの状態を変更しないため、constメンバ関数として定義できます。

関数内でm_techniqueNameを変更しようとすると、コンパイルエラーになります。

m_techniqueName = "変更された技";

constメンバ関数では、通常のメンバ変数を変更できません。

constメンバ関数とthisポインタ

通常のメンバ関数では、thisは現在のオブジェクトを指すポインタです。

概念的には、次のような型として扱われます。

WarriorProfile* const this

this自体が指す先を変更できませんが、thisが指しているオブジェクトの状態は変更できます。

一方、constメンバ関数のthisは、概念的に次のように考えられます。

const WarriorProfile* const this

this自体の指す先を変更できないだけでなく、thisが指しているWarriorProfileオブジェクトも変更できません。

そのため、constメンバ関数内では、通常のメンバ変数を書き換えられません。

main関数で動作を確認する

main.cppでは、WarriorProfileオブジェクトを作成します。

最大気力を表示したあと、技名と現在気力を設定します。

最後に、const参照を受け取るshowReadOnlyProfileへオブジェクトを渡します。

プロジェクト/ファイル名: Chap7_03/main.cpp

#include <iostream>
#include "warrior_profile.h"

using namespace std;

// 戦士の情報を読み取り専用で受け取る関数
void showReadOnlyProfile(const WarriorProfile& profile);

int main(int argc, char** argv) {
    // クラス全体で共有する定数を表示する
    cout << "最大気力の定数:"
         << WarriorProfile::MAX_ENERGY << endl;

    // 戦士情報を管理するオブジェクト
    WarriorProfile warrior;

    // 技名と現在気力を設定する
    warrior.setTechniqueName("蒼天気功");
    warrior.setEnergy(85);

    // const参照で戦士情報を表示する
    showReadOnlyProfile(warrior);

    return 0;
}

// 渡されたオブジェクトの情報を読み取る
void showReadOnlyProfile(const WarriorProfile& profile) {
    cout << "==== 戦士情報 ====" << endl;

    // constメンバ関数なので呼び出せる
    profile.showStatus();

    // 状態を変更する関数は呼び出せない
    // profile.setEnergy(100);

    cout << "==================" << endl;
}

実行結果

最大気力の定数:100
==== 戦士情報 ====
技名:蒼天気功
現在気力:85
最大気力:100
==================

プログラムの流れ

プログラムでは、最初にMAX_ENERGYをクラス名から表示しています。

WarriorProfile::MAX_ENERGY

MAX_ENERGYはstaticメンバなので、WarriorProfileオブジェクトを作る前でも利用できます。

次に、WarriorProfileオブジェクトを作成します。

WarriorProfile warrior;

技名を設定します。

warrior.setTechniqueName("蒼天気功");

現在気力を設定します。

warrior.setEnergy(85);

最後に、warriorをconst参照でshowReadOnlyProfileへ渡します。

showReadOnlyProfile(warrior);

showReadOnlyProfileの仮引数は次の形です。

const WarriorProfile& profile

そのため、profileを通してwarriorの情報を読み取ることはできますが、変更はできません。

const参照から呼び出せるメンバ関数

showReadOnlyProfileのprofileは、const WarriorProfile&です。

そのため、constメンバ関数は呼び出せます。

profile.showStatus();

showStatusは次のようにconst指定されています。

void showStatus() const;

一方、setEnergyはオブジェクトの状態を変更するため、constメンバ関数ではありません。

void setEnergy(int energy);

そのため、次の呼び出しはできません。

profile.setEnergy(100);

コンパイラは、読み取り専用のprofileを通してオブジェクトを変更しようとしていると判断し、エラーを出します。

constオブジェクト

オブジェクトそのものをconst指定することもできます。

const WarriorProfile warrior;

constオブジェクトは、生成後に状態を変更できません。

そのため、constメンバ関数だけを呼び出せます。

warrior.showStatus();

showStatusはconstメンバ関数なので呼び出せます。

一方、次の関数は呼び出せません。

warrior.setEnergy(80);

setEnergyはm_energyを変更する関数だからです。

constオブジェクトとメンバ関数の関係は、次のように整理できます。

オブジェクトconstメンバ関数通常のメンバ関数
通常のオブジェクト呼び出せる呼び出せる
constオブジェクト呼び出せる呼び出せない
const参照呼び出せる呼び出せない
constポインタ呼び出せる呼び出せない

constメンバ関数の宣言と定義

constメンバ関数では、ヘッダーファイルの宣言とcppファイルの定義の両方にconstが必要です。

ヘッダーファイルでは、次のように書きます。

int getEnergy() const;

cppファイルでは、次のように書きます。

int WarriorProfile::getEnergy() const {
    return m_energy;
}

宣言にだけconstを付け、定義からconstを省略すると、別の関数として扱われてしまうため、正しく定義できません。

場所正しい記述
クラス内の宣言int getEnergy() const;
cppファイルの定義int WarriorProfile::getEnergy() const

constの位置で意味が変わるポインタ

ポインタでは、constを書く位置によって意味が変わります。

指している値を変更できないポインタ

const int* power;

または次のようにも書けます。

int const* power;

この場合、powerが指している値を変更できません。

ただし、power自身が別の変数を指すように変更することはできます。

ポインタ自身を変更できない場合

int* const power = &energy;

この場合、powerが指しているenergyの値は変更できます。

ただし、powerを別の変数へ向けることはできません。

値もポインタも変更できない場合

const int* const power = &energy;

この場合、指している値も、ポインタの指す先も変更できません。

宣言指している値ポインタの指す先
const int* power変更できない変更できる
int* const power変更できる変更できない
const int* const power変更できない変更できない

図:constを使う場所と守られる範囲

この図が示していること

この図では、constを付ける場所によって、何が保護されるのかを整理しています。

変数へconstを付けると、その値を変更できません。

ポインタや参照が指す型へconstを付けると、そのポインタや参照を通して対象のオブジェクトを変更できません。

メンバ関数の末尾へconstを付けると、その関数内でオブジェクトの状態を変更できません。

static constメンバを使うと、クラス全体で共有する変更不可能な値を定義できます。

constメンバ関数内で変更しようとした場合

次のshowStatusはconstメンバ関数です。

void WarriorProfile::showStatus() const {
    cout << m_energy << endl;
}

この関数内でm_energyを変更しようとすると、コンパイルエラーになります。

void WarriorProfile::showStatus() const {
    m_energy = 100;
}

showStatusは読み取り専用の関数として宣言されているためです。

同じように、constメンバ関数から通常のメンバ関数を呼び出すことも、基本的にはできません。

void WarriorProfile::showStatus() const {
    setEnergy(100);
}

setEnergyはm_energyを変更する可能性があるため、constメンバ関数からは呼び出せません。

一方、別のconstメンバ関数は呼び出せます。

void WarriorProfile::showStatus() const {
    cout << getEnergy() << endl;
}

getEnergyもconstメンバ関数なので、安全に呼び出せます。

const参照で引数を受け取る利点

クラスオブジェクトを関数へ渡す場合、値渡しではオブジェクトのコピーが作られます。

void showProfile(WarriorProfile profile);

WarriorProfileが多くのメンバ変数を持つ大きなクラスだった場合、コピーにも処理が必要です。

const参照なら、コピーを作らずに元のオブジェクトを参照できます。

void showProfile(const WarriorProfile& profile);

さらに、constによって関数内からの変更も禁止できます。

引数の渡し方コピー元のオブジェクトの変更
WarriorProfile profile作られる変更されない
WarriorProfile& profile作られない変更できる
const WarriorProfile& profile作られない変更できない

読み取るだけの関数では、const参照が安全で効率的な選択になります。

constを使う意味

constの最も大きな役割は、変更してはいけないデータをプログラム上で明確にすることです。

コメントで値を変更しないでくださいと書くだけでは、コンパイラは変更を止めてくれません。

constを付けておけば、誤って変更するコードを書いた時点でコンパイルエラーになります。

constによる効果内容
誤操作の防止変更してはいけない値への代入を禁止する
意図の明確化読み取り専用であることをコード上で示す
関数の役割を明確にする状態を読む関数と変更する関数を区別できる
constオブジェクトへの対応読み取り専用オブジェクトから安全に呼び出せる
不要なコピーの回避const参照によって大きなオブジェクトを効率よく渡せる

constと処理速度の関係

constを付けると必ず処理速度が向上する、というわけではありません。

constは、主にプログラムの安全性と設計上の意図を明確にするための機能です。

コンパイラがconstの情報を最適化に利用できる場合もありますが、constを付けただけで必ず高速になるとは限りません。

特に大きなオブジェクトをconst参照で渡す場合は、値渡しによるコピーを避けられるため、処理量やメモリ使用量を減らせる可能性があります。

void showProfile(const WarriorProfile& profile);

この場合の効率向上は、constそのものよりも、参照によってコピーを避けていることが大きな理由です。

constを使う第一の目的は、変更してはいけないものを守り、間違いを早い段階で発見することだと考えるとよいでしょう。

constを適切に使う考え方

すべての変数や関数にconstを付ければよいわけではありません。

変更する必要があるものには、通常の変数や通常のメンバ関数を使います。

変更する必要がないものには、constを付けます。

状況選ぶ形
途中で値を変更する通常の変数
初期化後に値を固定するconst変数
オブジェクトを変更する関数通常の参照
オブジェクトを読むだけの関数const参照
メンバ変数を変更する関数通常のメンバ関数
メンバ変数を読むだけの関数constメンバ関数
クラス全体で共有する固定値static constメンバ

ドラゴンボール風に考えるなら、現在気力は修行や戦闘で変化するため、通常の変数として管理します。

最大気力は戦士の管理規則として固定するため、constで守ります。

気力を回復する処理は状態を変更するため、通常のメンバ関数にします。

気力を表示するだけの処理は、状態を変更しないため、constメンバ関数にします。

const修飾子で身につけたい感覚

constは、値を単に変更できなくするだけの機能ではありません。

その変数、引数、オブジェクト、メンバ関数が、どのような役割を持っているのかを明確にするための仕組みです。

const int MAX_ENERGY = 100;

このconstは、MAX_ENERGYが変更されない値であることを表します。

void showProfile(const WarriorProfile& profile);

このconstは、showProfileが渡されたWarriorProfileを変更しないことを表します。

int getEnergy() const;

このconstは、getEnergyがオブジェクトの状態を変更しないことを表します。

static const int MAX_ENERGY = 100;

このstatic constは、クラス全体で共有する変更不可能な値であることを表します。

ドラゴンボール風にたとえるなら、constは重要な気力データや修行記録を守る保護結界です。

見ることはできても、許可なく変更することはできません。

値を変更する必要がある処理と、読み取るだけの処理を明確に分けることで、クラスの役割が分かりやすくなります。

変更してはいけない場所へconstを付ける習慣を身につけると、意図しない値の変更をコンパイル時に発見でき、安全で読みやすいC++プログラムを作れるようになります。

今回の構成では、処理速度に関する説明を断定せず、constの中心的な役割である安全性と設計意図を重視しています。