const の活用 | Programming Place Plus C++編【言語解説】 第15章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


constオブジェクト 🔗

この章では、const修飾子(以降、単に const と記述します)とそれに関連する話題を取り上げます。C++ において、const の利用価値は非常に高く、使える場面では積極的に使うべきです

まずは、もっとも単純と思われる使い方から見ていきましょう。

int main()
{
    const int a = 10;

    a = 20;  // エラー
}

変数a は const付きですから、初期化した後は、新たな値で上書きすることができなくなります。この使い方は、C言語と C++ とで違いがあります。

int main()
{
    const int a = 10;
    int array[a];  // C言語ではエラー、C++ では OK
}

C言語で const を付けて定義された変数は、書き換えができない変数という扱いであり、定数としては使えません。そのため、配列の要素数のように、定数を要求する場面には使用できません。

クラス型のオブジェクトに関しては、const が付いていると、メンバ変数を書き換えることができません。さらに、メンバ関数^については constメンバ関数(第12章)でないと呼び出せなくなります。constメンバ関数については、この章でも再度取り上げます

const が付いていても、コンストラクタやデストラクタの呼び出しには影響を与えません。

内部結合と外部結合 🔗

グローバルの constオブジェクトが、内部結合か外部結合なのかというルールが、C言語と C++ とで異なっています。

C言語の場合、static指定子を付ければ内部結合になりますが、付けなければ外部結合になります。C++ では、extern指定子を付ければ外部結合になり、付けなければ内部結合になります。つまり、static や extern を付けたときは同じ結果ですが、何も付けない場合の意味が異なっています。

このルールは、(無名名前空間を除いて)名前空間スコープの constオブジェクトでも同様です。無名名前空間は、いつも内部結合です。

プログラム全体で使いたいような定数をヘッダファイルに置くときには、ソースファイル側に定義を書き、ヘッダファイル側に extern指定子付きの宣言を書くのが良いです。

// data.cpp
#include "data.h"

const int DATA_VALUE = 100;
// data.h
#ifndef DATA_H_INCLUDED
#define DATA_H_INCLUDED

extern const int DATA_VALUE;

#endif
// main.cpp
#include <iostream>
#include "data.h"

int main()
{
    std::cout << DATA_VALUE << std::endl;
}

実行結果:

100

このようにすると、data.h を複数の箇所でインクルードしたとしても、実体は data.cpp にある1つだけになるので、プログラムサイズの増加を防げます。また、後で初期値を変更したとしても、data.h を修正する必要がないので、data.cpp 以外には再コンパイルの必要もありません。

当然ではありますが、名前空間で囲む場合は、定義と宣言のいずれも同じ名前の名前空間で囲む必要があります。

// data.cpp
#include "data.h"

namespace data {
    const int VALUE = 100;
}
// data.h
#ifndef DATA_H_INCLUDED
#define DATA_H_INCLUDED

namespace data {
    extern const int DATA;
}

#endif
// main.cpp
#include <iostream>
#include "data.h"

int main()
{
    std::cout << data::VALUE << std::endl;
}

実行結果:

100


constメンバ変数 🔗

メンバ変数は const にできます。この場合、コンストラクタの本体コードで初期化できません(代入になってしまうので)。そのため、メンバイニシャライザ(第13章)を使って初期化する必要があります。

class MyClass {
public:
    MyClass();

private:
    const int mValue;
};

MyClass::MyClass() : mValue(10)
{
    mValue = 10;  // コンパイルエラー
}

constメンバ変数は、コンストラクタの引数で外部から与えられた情報を、うっかり変更してしまわないように、大切に取っておくために利用できます。

constメンバ変数があると、オブジェクトをコピーする行為を邪魔することには注意してください。

class MyClass {
public:
    explicit MyClass(int v) : mValue(v)
    {}

private:
    const int mValue;
};

int main()
{
    MyClass c1(10);
    MyClass c2(20);

    c1 = c2;  // コンパイルエラー
}

【上級】クラス型のオブジェクトに代入するには、operator=() が必要です(第17章)。通常は、暗黙的に作られますが、コピーできないメンバ変数が含まれていると作られないため、コピーしようとするとコンパイルエラーになってしまいます。

constメンバ関数 🔗

メンバ関数の宣言のとき、後ろに const を付けることによって、constメンバ関数になります。これは、第12章でも説明しました。

// 宣言
戻り値の型 メンバ関数名(仮引数の並び) const;

// 定義
戻り値の型 クラス名::メンバ関数名(仮引数の並び) const
{
}

【上級】staticメンバ関数第18章)には const を付加できません。

constオブジェクト、constポインタ、const参照(第16章)からメンバ関数を呼び出すときには、constメンバ関数しか呼び出せません

コンストラクタやデストラクタは、const の有無とは関係なく呼び出されます。これらの関数を constメンバ関数にはできません。

動作を確認してみます。

#include <iostream>

class Student {
public:
    Student() : mScore(0)
    {}

    inline void SetScore(int score)
    {
        mScore = score;
    }

    inline int GetScore() const  // constメンバ関数
    {
        return mScore;
    }

private:
    int      mScore;
};

int main()
{
    Student s1;
    const Student s2;
    Student* p1 = &s1;
    const Student* p2 = &s2;

    s1.SetScore(80);
    s2.SetScore(65);    // コンパイルエラー
    p1->SetScore(75);
    p2->SetScore(50);   // コンパイルエラー

    std::cout << s1.GetScore() << std::endl;
    std::cout << s2.GetScore() << std::endl;
    std::cout << p1->GetScore() << std::endl;
    std::cout << p2->GetScore() << std::endl;
}

単純な、よくある説明の仕方をすれば、constメンバ関数内ではメンバ変数を書き換えることが禁止されます。もう少し実際の挙動に即して言えば、constメンバ関数内では、thisポインタが constポインタになるということです。

たとえば、this-&gt;mScore = 10; のように書いたとき、this が constポインタならば、ポインタが指し示す先にあるものを書き換えられないため、結果的にメンバ変数の書き換えはできません。

thisポインタの部分を省略して mScore = 10; と書いたとしても、これは、this-&gt; の部分の記述を省略しているだけであって、違いはありません。

また、constメンバ関数から、別のメンバ関数を呼び出す場合、呼び出す先も constメンバ関数でなければなりません。これも、this が constポインタになっているのだと理解していれば、this-&gt;f(); ができなければならないという点から理解できるでしょう。

constメンバ関数と非constメンバ関数とのあいだでオーバーロードできます。もし、constオブジェクトから呼び出そうとすれば、constメンバ関数の方が選択されますし、非constオブジェクトから呼び出そうとすれば、非constメンバ関数の方が選択されます。

このようなオーバーロードを行う場合に、constメンバ関数と、非constメンバ関数の実装が同じになってしまうことがあります。まったく同じコードを2か所に書くのは保守面から望ましくありません。そこで、次のように書く方法があります。

#include <iostream>

class MyClass {
public:
    explicit MyClass(int v) : mValue(v)
    {
    }

    inline int* Get()
    {
        return const_cast<int*>(static_cast<const MyClass*>(this)->Get());
    }

    const int* Get() const;

private:
    int  mValue;
};

const int* MyClass::Get() const
{
    return &mValue;
}

int main()
{
    MyClass c(100);
    MyClass* p = &c;
    const MyClass* cp = &c;

    int* pv = p->Get();         // 非constメンバ関数を呼ぶ
    const int* pcv = cp->Get(); // constメンバ関数を呼ぶ

    std::cout << *pv << std::endl;
    std::cout << *pcv << std::endl;
}

実装は、constメンバ関数の方だけに書き、非constメンバ関数は constメンバ関数の方を呼び出す形にします。constメンバ関数の方に書くのは、制約が強い側で書いた方が、コンパイラのチェックが入り安全だからです。逆にしてしまうと、const が台無しになります。

非constメンバ関数から、constメンバ関数を呼び出すには工夫が必要です。単純に書くと、自分自身を呼び出して再帰呼び出しになってしまいます。

そこでまず、thisポインタに明示的に const を付けます。そのためには、「static_cast<const MyClass*>(this)」というように、static_cast を使います

const_cast でも const を付けることはできます。ただ、const_cast には、const を外している危なそうな箇所を探しやすくする価値があるので、外すとき専用にしておいた方がいいような気はします。

const を付けた thisポインタを経由して関数呼び出しを行えば、意図どおり、constメンバ関数版の Get関数を呼び出せます。

戻り値があるのなら、constメンバ関数が返した戻り値を、const_cast で const を取り除いて返します。原則として const_cast は使わないことが望ましいですが、この場面では、constメンバ関数内で何をしているか、プログラマー自身で分かっている訳ですし、そもそも元をただせば、呼び出したものは非const版なのだから、const を外す行為に問題はありません。


mutableキーワード 🔗

constメンバ関数は非常に重要かつ有用な機能ですが、まれに、思うように使えない場面があります。

たとえば、結果をキャッシュしておくように実装する場合が挙げられます。

class Accessor {
public:
    Accessor();
    ~Accessor();

    const void* Get() const;

private:
    const void* mData;
};

Accessor::Accessor() : mData(NULL)
{
}

Accessor::~Accessor()
{
    delete [] mData;
}

const void* Accessor::Get() const
{
    if (mData == NULL) {
        mData = new char[1 * 1024 * 1024];  // constメンバ関数内ではメンバ変数を書き換えられない。
    }
    return mData;
}

int main()
{
    Accessor accessor;
    accessor.Get();
}

return するデータを作り出すこと自体が、非常にコストが掛かるものであり(たとえば、ネットワークを通してデータを取得してこなければならないだとか、単に非常に巨大であるだとか)、また、条件分岐の仕方によっては、そのデータが使われないこともあるのであれば、初めて Accessor::Get関数が呼び出されたときにだけ、データを作ることが望ましいでしょう。

そのような要件で実装する場合、Accessor::Get関数の中でデータを作りたいところですが、そうすると、メンバ変数 mData を書き換える必要が出てくるため、constメンバ関数にできなくなってしまいます。

Accessor::Get関数は、返すべきデータがすでにあれば、単にそのデータのメモリアドレスを return するだけです。この章の冒頭で書いたとおり、const が使えるのなら積極的に使うべきですし、ゲッター系のメンバ関数は、constオブジェクトからでも呼び出せた方がいいでしょうから、constメンバ関数にしたいところです。

この問題を解決するには、普通に考えれば、次のようにメンバ関数を分離するしかありません。

class Accessor {
public:
    Accessor();
    ~Accessor();

    void CreateData();
    const void* Get() const;

private:
    const void* mData;
};

Accessor::Accessor() : mData(NULL)
{
}

Accessor::~Accessor()
{
    delete [] mData;
}

void Accessor::CreateData()
{
    if (mData == NULL) {
        mData = new char[1 * 1024 * 1024];
    }
}

const void* Accessor::Get() const
{
    return mData;
}

int main()
{
    Accessor accessor;
    accessor.Get();
}

こうするとまず、オブジェクトが const であるのなら Accessor::CreateData関数を呼び出せない問題があります。また、Accessor::CreateData関数を呼び忘れないようにしなければならないので、このクラスの使い方がシンプルでなくなってしまいます。

そこで、mutableキーワードを使う方法があります。mutable は、メンバ変数の宣言時に付加します。

class X {
    mutable 型名 メンバ変数名;
};

mutable が付加されたメンバ変数は、constオブジェクトであっても書き換え可能です。そのため、constメンバ関数内からでも書き換えることができます。

struct X {
    int v1;
    mutable int v2;
};

int main()
{
    const X cx;
    cx.v1 = 100;  // コンパイルエラー
    cx.v2 = 100;  // OK
}

mutable を使って Accessorクラスを直すと、次のようになります。

#include <iostream>

class Accessor {
public:
    Accessor();
    ~Accessor();

    const void* Get() const;

private:
    mutable const void* mData;  // constメンバ関数からでも書き換えられる
};

Accessor::Accessor() : mData(NULL)
{
}

Accessor::~Accessor()
{
    delete [] mData;
}

const void* Accessor::Get() const
{
    if (mData == NULL) {
        mData = new char[1 * 1024 * 1024];  // OK
    }
    return mData;
}

int main()
{
    Accessor accessor;

    std::cout << accessor.Get() << std::endl;
    std::cout << accessor.Get() << std::endl;
}

実行結果:

014D9040
014D9040

mutable は、その存在意義を正しく理解しないと、誤った捉え方・使い方をしてしまうでしょう。mutable は、せっかくの const による安全対策に穴を空けているように思えます。

const の意味を、オブジェクトを「1ビットたりとも書き換えない」のだと捉えるのであれば、mutable は確かにおかしな機能です。しかし、「クラスの外から変更の有無が分からなければ、書き換えても構わない」と捉えるのなら、mutable は(使い方を誤らなければ)問題のない機能です。

このような定数性の考え方を、論理的定数性と呼ぶことがあります。

たとえば、関数を呼び出した回数をカウントしておくだとか、計算結果をキャッシュしておいて、次回以降は計算をスキップして結果だけ返すようにするといった使い方は適切です。

このようなケースで mutable を使わないことにこだわるあまり、const を使うこと自体を諦めてしまうとか、const_cast を使うといった手段に走るのなら、むしろ悪い方向に進んでいるといえるでしょう。


C++11 (constexpr指定子) 🔗

C++11 🔗

C++11 には、constexpr指定子 が追加されています。constexpr指定子が使える場面はいくつかありますが、共通している考え方は、コンパイル時に評価できる定数式を表しているということです。

まず、変数宣言時に constexpr を使う、constexpr変数から見てみましょう。const の場合と比較してみます。

#include <iostream>

int main()
{
    int x = 0;             // 定数式でない

    const int a = 10;
    const int b = a;
    const int c = x;       // const では許される

    constexpr int aa = 10;
    constexpr int bb = aa;
//    constexpr int cc = x;  // x は定数式でないのでエラー

    constexpr int dd = a;  // OK
//    constexpr int ee = c;  // c は(c を初期化している x が)定数式でないのでエラー
}

constexpr変数は、その初期値をコンパイル時に決定できなければなりません。「10」のようなリテラル値や、constexpr が付いた他の定数を使って初期化できます。

上の例で、コメントアウトされている2つの方法ではコンパイルエラーになります。cc は x で初期化しようとしていますが、x はただの変数であり、コンパイル時に値が確定していないのでエラーになります。

ee は c で初期化しようとしています。c は const付きですが、その c の初期値である x が定数でないため、結局 cc のときと同様に、コンパイル時に初期値が確定できずエラーになります。

constexpr は関数にも付けられます。これは、constexpr関数と呼びます。constexpr関数は、実引数がすべてコンパイル時に確定できる場合、関数呼び出し全体を定数式として扱えます

コンパイル時に確定できればいいので、関数テンプレートを constexpr関数にすることも可能です。

constexpr関数の実引数がコンパイル時に確定できない場合は、単に、通常の関数のように振る舞います。

constexpr関数は、ある程度複雑な計算を含んでいても、コンパイル時にその計算処理を終えられるので、実行時のパフォーマンスを大幅に向上できる可能性を秘めており、非常に強力な機能と言えます。ただ、以下のように制約も多くなっています。

  • ローカル変数を使えない。
  • 非ローカル変数を参照できるが、書き換えてはならない。
  • if や switch が使えない (?: は可)
  • ループが使えない
  • return文を1つだけ含んでいる必要がある(?: 以外の方法で分岐できないので、必然的に return文だけの関数になる)

制約は厳しいですが、if や switch が使えないことは ?: で代用できますし、ループの代わりに再帰関数呼び出しで代用できるので、一応何でも書けます。

実際の使用例は次のようになります。

#include <iostream>

constexpr int func(int a, int b)
{
    return a * 10 + b;
}

int main()
{
    constexpr int a = func(3, 5);  // constexpr関数は constexpr の定数の初期化に使える

    int x = 7;
    int b = func(a, x);  // constexpr関数の実引数がコンパイル時に確定できなくても、コンパイル可能

    std::cout << a << "\n"
              << b << std::endl;
}

実行結果:

35
357

メンバ関数も constexpr関数にできます。(C++11 では)この場合、暗黙的に constメンバ関数になるので、宣言の後ろに付ける const修飾子は不要です。

class MyClass {
public:
    inline constexpr int GetMaxValue()  // constexprメンバ関数
    {
        return 100;
    }
};

コンストラクタにも constexpr を付けられます。これは、constexprコンストラクタと呼び、そのクラスをリテラル型として使用できるようにします。リテラル型のクラスのオブジェクトは、定数式として使用できます。

#include <iostream>

class MyClass {
public:
    constexpr MyClass(int max) : mMaxValue(max)
    {}

    inline constexpr int GetMaxValue()
    {
        return mMaxValue;
    }

private:
    const int mMaxValue;
};

constexpr int func(MyClass a)
{
    return a.GetMaxValue() * 10;
}

int main()
{
    MyClass a = 10;

    std::cout << a.GetMaxValue() << "\n"
              << func(a) << std::endl;
}

実行結果:

10
100


練習問題 🔗

問題① 次のようなメンバ関数は適切と言えるでしょうか?

class DataStore {
public:
    inline int* GetValue() const
    {
        return &mValue;
    }

private:
    int    mValue;
};


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 VisualStudio 2015 の対応終了。

 第18章と内容を入れ替えた(以下の履歴は、旧第18章のもの)

 「内部結合と外部結合」の項の内容を整理。名前空間の話題を補った。
constexpr を指定子と表記するように修正。

 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

≪さらに古い更新履歴を展開する≫



前の章へ (第14章 動的なオブジェクトの生成)

次の章へ (第16章 参照)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る