クラス | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、クラスを取り上げます。クラスはオブジェクト指向プログラミングに登場する重要な概念ですが、現代の C++ では、オブジェクト指向にはあまりこだわる必要はなく、C++ がもつ機能の1つとして、ほかの機能と同じように学べばいいでしょう。新C++編ではこの方針で解説を行います。クラスに関連する機能は非常に多いので、このページではまず基本的な部分を取り上げ、この先のページで少しずつほかの機能を解説していきます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



クラス

ここから先のページでは、本格的にクラス (class) を使っていきます。

これまでのページでもクラスという言葉自体は何度か登場しており、「(C++ では)構造体と実質的に同じもの」という説明をしました(「構造体」のページを参照)。これは間違っていないですが、ではどのように使い分ければいいのかということになります。よくある使い分けは、「データの集まり」なら構造体、「データ以外のものも含んだ集まり」ならクラスを使うという考え方です。

C++ の構造体とクラスの唯一の違いは、デフォルトのアクセス指定子です。この話題は後で登場します

「データ以外のものも含んだ集まり」と書きました。データ以外というのは具体的には、関数や型の定義のことです。関数を含んだ例として、「シャッフルと乱数」や「関数ポインタとラムダ式」のページで取り上げた関数オブジェクトがあります。関数オブジェクトには operator() という変わった名前の関数が含まれていました。クラス内で宣言されている関数はメンバ関数 (member function) と呼ばれます。メンバ関数の存在が、「データの集まり」としての構造体との最大の違いといえるでしょう。

ところで、クラスといえばオブジェクト指向プログラミング (object-oriented programming) を連想する人も多いでしょう。オブジェクト指向プログラミングは重要ではありますが、完全に正しいかたちで実現することは非常に困難であるという現実があります(それ以前に正しいオブジェクト指向とは?というこれまた非常に難しい話もありますが)。そういうわけで、新C++編としては、オブジェクト指向プログラミングについては特に取り上げるつもりはありません。「クラス」は C++ が持つ数ある機能の1つにすぎないものとして解説を進めます。

クラスには非常に多くの機能が存在します。たとえば、メンバへのアクセスに制限をかけるアクセス指定子 (access specifier) (このページで取り上げます)、オブジェクトが作られるときに自動的に呼び出される特殊なメンバ関数であるコンストラクタ (construtor) (このページで取り上げます)、オブジェクトが破棄されるときに自動的に呼び出される特殊なメンバ関数であるデストラクタ (destructor)、ほかのクラスがもつメンバを受け継いで使えるようにする継承 (inheritance)、また受け継いだ先でメンバ関数の内容を書き換えるオーバーライド (override) といったものがあります。こうした機能の多くはオブジェクト指向プログラミングを実現するために存在しているわけですが、個別の機能を理解していくために、いちいちオブジェクト指向プログラミングの難しい部分に立ち入る必要はないと思います。

クラスを定義する

クラスは構造体と実質的に同じものなので、使用するための手順も同じです。まずクラスという型を定義し、その型の変数を定義すればいいわけです。

クラスを定義する構文は次のとおりです。

class クラス名 {
    メンバの宣言;
      :
};

class は、これがクラスの定義であることを表すキーワードです。

たとえば、色を表現する Colorクラスを次のように定義できます。

class Color {
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

{} の内側に、データメンバやメンバ関数(後述)を記述できます。

classstruct に変更するだけで、構造体として正しい定義になります。ただし、class を使った場合は、メンバへのアクセスがデフォルトでは禁じられた状態になります。メンバへのアクセスを禁じているものは、このあと取り上げるアクセス指定子です。

アクセス指定子

さきほどの Colorクラス定義は、実際には次のように書くのと同じです。

class Color {
private:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

privateアクセス指定子 (access pecifier) の一種です。コードに記述するときには、: も必要です。アクセス指定子には private以外に、publicprotected があります。

アクセス指定子は、そこからうしろに宣言されたメンバへのアクセスの自由度を指定するものです。

アクセス指定子 意味 補足
public どの関数からであってもアクセスできる(公開) struct でのデフォルト
private 同じクラスのメンバ関数からでなければアクセスできない(非公開) class でのデフォルト
protected 同じクラス、または派生クラスからでなければアクセスできない(限定公開)

protected を理解するためには、継承という機能を解説しなければならないので、ここではまだ取り上げません。現時点で重要なのは、private と public の2つです。

public と指定されているメンバには、(スコープ内にあれば)どの関数からでもアクセスできます。このようなメンバのことを「public なメンバ」とか「公開されているメンバ」のように表現することがあります。public は構造体(つまり struct)の場合のデフォルトのアクセス指定子です。次のコードでは、Colorクラスの public なメンバに、main関数からアクセスしています。

class Color {
public:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

int main()
{
    Color color {};
    color.red = 255;                 // OK
    std::cout << color.red << "\n";  // OK
}

private と指定されているメンバには、そのメンバを宣言しているクラスのメンバ関数からでなければアクセスできません。このようなメンバのことを「private なメンバ」とか「非公開のメンバ」のように表現することがあります。class のデフォルトは private です。次のコードでは、Colorクラスの private なメンバに、main関数からアクセスしようとしているため、コンパイルエラーになっています。

class Color {
private:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

int main()
{
    Color color {};
    color.red = 255;                 // コンパイルエラー
    std::cout << color.red << "\n";  // コンパイルエラー
}

これでは当然困るわけですが、ここでアクセス指定子を public に変えてしまうと構造体的な発想になってしまいます。public なメンバ関数を用意するのがクラス的な発想です。

メンバ関数

メンバ関数 (member function) は、クラスのメンバとして宣言されている関数のことです。

class クラス名 {
    戻り値の型 メンバ関数名(仮引数);
};

定義のほうは、クラス定義の内側でも外側でも書けますが、違いがあるので使い分けを考える必要があります。まずは、クラス定義の外側に記述する方法を紹介します。クラス定義の内側に記述する方法は、あとで改めて取り上げることにします。

メンバ関数の定義を、クラス定義の外側でおこなう場合は、次のように記述します。

戻り値の型 クラス名::メンバ関数名(仮引数)
{
    本体
}

クラス名:: があることで、通常の関数との区別を付けます。クラス定義が名前空間内にあるのなら、メンバ関数の定義も同じ名前空間内になければなりません。

ここで、クラス定義をヘッダファイルに記述している場合に、メンバ関数の定義を同じヘッダファイル内に書くのか、対応するソースファイル側に書くのかという選択肢があります。「ヘッダファイル」のページで取り上げたとおり、関数の定義はヘッダファイルには書かないことが基本原則であり、ここでもそれに従うべきです。そうすることで、関数の具体的な実装コードを、クラスの利用者から隠すことができ、あとから内部実装を変更しやすくなる利点が生まれます。

また、ヘッダファイルに実装コードを書くと、実装に必要なほかのヘッダファイルをインクルードしなければならない可能性が高くなります。すると、多くのヘッダファイルをインクルードすることになり、コンパイルにかかる時間が長くなる欠点もあります。


メンバ関数は、データメンバの関数版のような存在ですから、呼び出すためには、クラスから作られたオブジェクトが必要です。オブジェクトがあれば、データメンバと同様に、. を使って、オブジェクト名.メンバ関数名(実引数) のように呼び出せます。オブジェクトのポインタからであれば、オブジェクトを指すポインタ->メンバ関数名(実引数) となります。

#include <iostream>

class C {
public:
    void f(int v);
};

void C::f(int v)
{
    std::cout << v << "\n";
}

int main()
{
    C c {};     // クラスC のオブジェクト c を定義
    c.f(100);

    C* pc {&c};
    pc->f(200);
}

実行結果:

100
200

メンバ関数の本体から、同じクラスに属しているほかのメンバ関数を呼び出すときには、通常の関数のように メンバ関数名(実引数) の構文で呼び出せます。最初のメンバ関数を呼び出すときに、どのオブジェクトが対象になっているのかは確定しているためです。

#include <iostream>

class C {
public:
    void f1(int v);
    void f2(int v, int v2);
};

void C::f1(int v)
{
    f2(v, 0);  // OK。f1 を呼び出したのが c なら、これは c に対して f2 を呼ぶ
}

void C::f2(int v, int v2)
{
    std::cout << v << ", " << v2 << "\n";
}

int main()
{
    C c {};
    c.f1(100);
}

実行結果:

100, 0

ここで、f2メンバ関数を呼び出している箇所よりも、f2メンバ関数の宣言のほうが後ろにありますが、きちんと f2関数を発見してコンパイルできます。これは、「スコープと名前空間」のページで取り上げた、クラススコープの特殊性によるものです。

もし、メンバ関数と同名の関数がクラスの外にある場合、クラスの外にある関数のほうが隠されることになります。隠された関数を呼び出す場合は、スコープ解決演算子(::) を用います。

#include <iostream>

void f2()
{
    std::cout << "::f2()\n";
}

class C {
public:
    void f1();
    void f2();
};

void C::f1()
{
    f2();
    ::f2();
}

void C::f2()
{
    std::cout << "C::f2()\n";
}

int main()
{
    C c {};
    c.f1();
}

実行結果:

C::f2()
::f2()


テーマプログラムであるペイントスクリプトに話題を戻して、キャンバスをクラスにすることを考えてみます。

うまくクラスを作るときに意識することは、「クラスを作る側のプログラマーと、クラスを使う側のプログラマーが別人であることを想像する」ことです。もちろん、実際にはどちらも自分なのかもしれませんが、作者側と利用者側を分けて考えると、使いやすく、間違いが起きにくく、直しやすいプログラムを実現できる可能性が高まります。

基本原則として、データメンバは private にして直接触らせないようにして、代わりに最低限必要な処理を public なメンバ関数として提供(公開)するようにします。こうすることで、作者側が意図していない使われ方を防げますし、利用者側がすでに書いているコードに影響を与えずに private な部分を変更することが容易になります。また、利用者側がクラスの使い方を迷いにくくなる効果もあります。

そこでたとえば、次のように Canvasクラスを作ることが考えられます。

class Canvas {
public:
    // 塗りつぶす
    void fill(Color color);

    // 点を描画する
    void paint_dot(int x, int y, Color color);

    // 四角形を描画する
    void paint_rect(int left, int top, int right, int bottom, Color color);

private:
    std::vector<std::vector<Color>>  pixels;    // キャンバスのピクセルの並び
}

キャンバスに対して行えることが明確になっており、使い方に迷うことも誤ることもありません。もし、pixels が public になっていたら、利用者側でピクセルを自由に操作できますが、ピクセルがどのように並んでいるのかという知識を要求することになります。キャンバスの範囲外に書き込もうとする事故を起こしかねませんし、ピクセルの並びを std::vector<std::vector<Color>> で表現していることを前提にコードを書いてしまいますから、あとから生の二次元配列に変更するような修正も難しくなります。

pixels を public にする代わりに、次のようなメンバ関数を提供することを考えるかもしれません。

class Canvas {
public:
    // キャンバス内のピクセルの並びへの参照を返す
    std::vector<std::vector<Color>>& get_pixels();

private:
    std::vector<std::vector<Color>>  pixels;
}

std::vector<std::vector<Color>>& Canvas::get_pixels()
{
    return pixels;
}

private なデータメンバへの参照やポインタを返す public なメンバ関数は便利なようにも思えますが、これではデータメンバを public にしていることと実質的に違いがありません。したがって先ほどの話と同じ問題に行き着きます。クラスの実装は圧倒的に楽になりますし、処理的にも効率がいいようにも思えますが、この方法は避けるべきものといえます。


次に Colorクラスの例です。private なデータメンバだけでは使いようがないので、public なメンバ関数を用意することが考えられます。

#include <iostream>

class Color {
public:
    void set_red(unsigned char r);
    void set_green(unsigned char g);
    void set_blue(unsigned char b);

    unsigned char get_red();
    unsigned char get_green();
    unsigned char get_blue();

private:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

void Color::set_red(unsigned char r)
{
    red = r;
}

void Color::set_green(unsigned char g)
{
    green = g;
}

void Color::set_blue(unsigned char b)
{
    blue = b;
}

unsigned char Color::get_red()
{
    return red;
}

unsigned char Color::get_green()
{
    return green;
}

unsigned char Color::get_blue()
{
    return blue;
}


int main()
{
    Color color {};
    color.set_red(255);
    color.set_green(0);
    color.set_blue(0);
    std::cout << static_cast<unsigned int>(color.get_red()) << ", "
              << static_cast<unsigned int>(color.get_green()) << ", "
              << static_cast<unsigned int>(color.get_blue()) << "\n";
}

実行結果:

255, 0, 0

RGB それぞれの強さを記憶しているデータメンバごとに、その値を設定、取得するメンバ関数を定義しています。このような、データメンバに値を代入することだけを目的にしたメンバ関数をセッター (setter) といい、データメンバの値を返すことだけを目的にしたメンバ関数をゲッター (getter) といいます。

セッターやゲッターは一見して分かりやすく、クラスを使うことに慣れていない初めのころは、あって当然のメンバ関数であるようにすら感じられるかもしれません。しかし、セッターやゲッターは積極的に作るものではなく、むしろ不要であれば作るべきではありません。

セッターやゲッターを公開することは、データメンバを公開していることと実質的に同じです。

c.set_red(100);
red = c.get_red();

// 実質、上と同じことでは?
c.red = 100;
red = c.red; 

つまり、クラスの重要な機能であるアクセス指定の意味を失くしてしまっており、構造体の発想に戻ってしまっているともいえます。これが意味することは、データメンバの使い方をクラスの利用者側に自由にさせてしまっているということです。前述の良くない実装の Canvasクラスと同じで、クラスの使用者が内部実装を理解しなければならず、使い方に戸惑い、間違った使い方をしてしまう恐れがあります。あとから内部実装を変更することも容易でなくなります。公開するメンバ関数は、クラスが提供する機能を表現したものであるべきであり、クラスを使う側はそれを使うだけの存在に徹するべきです

とはいえ、Colorクラスの例においては、セッターやゲッターを用意することも一つの解であるといえます。Colorクラスは、色の「データを表現する」ことが主な役割であって、どちらかといえば構造体に近い存在であるとも考えられるからです(したがって構造体にしておくことも選択肢の1つです)。このような「データの集まり」を、あえて構造体ではなくクラスによって表現すると、データメンバを操作するメンバ関数は単純なセッターやゲッターになりがちですが、これがメリットを生む場合があります。たとえば、不正な値を設定しようとしていないかをチェックするコードや、ログ出力を行うコードを仕込めることは代表的なメリットといえます。しかもそのようなコードは、あとから追加したり削除したりすることも容易です。

インライン関数

red = r; とか return red; のようなごく簡単な文1つで完結するメンバ関数の定義を、クラス定義の外側に記述することは非常に面倒に感じられます。前述しているように、メンバ関数の定義を、クラス定義の内側に書いてしまう方法も存在しており、この記法を使うとかなり楽になります。

構文は次のとおりです。

class クラス名 {
    戻り値の型 メンバ関数名(仮引数)
    {
        本体
    }
};

具体的なコードでは次のようになります。

class Color {
public:
    void set_red(unsigned char r)
    {
        red = r;
    }

    // ...

    unsigned char get_red()
    {
        return red;
    }

    // ...

private:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

この記法で書かれたメンバ関数は、インラインメンバ関数 (inline member function) です。インラインメンバ関数は、インライン関数であるメンバ関数のことです。

インライン関数 (inline function) は「ヘッダファイル」のページで少しだけ触れましたが、関数本体のコードを呼び出し元のコードに展開(インライン展開(インライン置換) (inline substitution))することによって、関数呼出しの実行時コストを消し去り、高速化を図ろうとする仕組みです。メンバ関数の場合も、通常の関数の場合も、inlineキーワード (inline keyword) を使うことで、インライン関数にできます。

class Color {
public:
    // クラス定義内で定義されたメンバ関数の場合は、inline の有無による違いはない
    inline void set_red(unsigned char r)
    {
        red = r;
    }
}

// 通常の関数は、inlineキーワードを付けて定義することで、インライン関数になる
inline void f()
{
}

しかし、インライン展開が行われるかどうかは最終的にコンパイラに任されますし、逆にインライン関数でなくても、コンパイラは同様の処置をおこなう可能性もあります1。つまり、インライン展開を期待してインライン関数にしても、せいぜいコンパイラにヒントを与える程度の効果しかありません。

インライン関数には、その定義が、呼び出しているすべての箇所から見えていなければならないという制約があります。そのため、ヘッダファイルに宣言だけ公開し、定義はソースファイル側に隠すということはできず、定義もヘッダファイルに書かなければなりません。これは関数の定義をヘッダファイルに書かないという基本原則から外れています。セッターやゲッター程度の関数ならば、やっていることが単純で、あとから大きく変更されることもないでしょうから、ヘッダファイルに公開されても特に問題はないですが、ほかの関数では、基本原則を破ってまで用いるものなのかどうかはよく考えなければなりません。

コンストラクタ

現状の Colorクラスには、データメンバを任意に初期化できない問題があります。構造体であれば許される次のコードがコンパイルできません。

Color color {255, 0, 0};  // コンパイルエラー

これはデータメンバが private だからです。public であれば許されますが、データメンバを public に変更することはクラス的な解決方法ではありません。

必要なのは、コンストラクタ (constructor) と呼ばれる特殊なメンバ関数です。コンストラクタは、次のように宣言します。

class クラス名 {
    クラス名(仮引数);  // コンストラクタ
};

通常のメンバ関数と同様、定義はクラス定義の外側でも内側でも記述できます。

コンストラクタには、通常のメンバ関数とは異なる点があります。

  1. 名前が自由に付けられず、必ずクラスと同じ名前にしなければならない
  2. オブジェクトを定義するときに自動的に呼び出され、ほかのタイミングでは使えない
  3. 戻り値がない

1番のルールがあるので、Colorクラスなら次のように Color という名前にしなければなりません。

class Color {
public:
    Color(unsigned char r, unsigned char g, unsigned char b);
}

コンストラクタは、オブジェクトが定義されるときに1度だけ自動的に呼び出されます。引数があるのなら、それに対応した実引数の記述が必要です。

Color color(255, 0, 0);

このような呼び出されかたであるため受け取る側が存在せず、戻り値がありません。戻り値の型を void と書くのではなく、記述自体ができないようになっています。

オブジェクトが定義されるときに確実に呼び出してくれるので、データメンバを確実に初期化することに使用します。

#include <iostream>

class Color {
public:
    Color(unsigned char r, unsigned char g, unsigned char b)
    {
        red = r;
        green = g;
        blue = b;
    }

    void set_red(unsigned char r)
    {
        red = r;
    }

    void set_green(unsigned char g)
    {
        green = g;
    }

    void set_blue(unsigned char b)
    {
        blue = b;
    }

    unsigned char get_red()
    {
        return red;
    }

    unsigned char get_green()
    {
        return green;
    }

    unsigned char get_blue()
    {
        return blue;
    }

private:
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

int main()
{
    Color color(255, 0, 0);  // 必ず初期化できる
    std::cout << static_cast<unsigned int>(color.get_red()) << ", "
              << static_cast<unsigned int>(color.get_green()) << ", "
              << static_cast<unsigned int>(color.get_blue()) << "\n";
}

実行結果:

255, 0, 0

ところで、コンストラクタの仮引数の名前を、rgb としていますが、redgreenblue としたほうが丁寧ではあります。しかしそうすると、データメンバの名前と衝突する問題が起こります。

Color(unsigned char red, unsigned char green, unsigned char blue)
{
    red = red;
    green = green;
    blue = blue;
}

これはコンパイルエラーにはなりませんが、正しく動作しません。スコープのルールから、データメンバよりも仮引数のほうが優先されることになり、red = red; は、仮引数の red の値を、同じく仮引数の red に自己代入していることになります。

この問題を解決するために、後述する thisポインタを使う方法があります。this-> と記述することによって、その先にあるものがデータメンバであることを明確にします。

Color(unsigned char red, unsigned char green, unsigned char blue)
{
    this->red = red;
    this->green = green;
    this->blue = blue;
}

別の解決方法として、単純に仮引数とデータメンバの名前が衝突しないように、命名規則を工夫することが挙げられます。よくあるのは、データメンバの名前に、メンバであることを意味する m とか m_ といったプリフィックスを付けるように統一する方法です。

class Color {
public:
    Color(unsigned char red, unsigned char green, unsigned char blue)
    {
        m_red = red;
        m_green = green;
        m_blue = blue;
    }

    // ...

private:
    unsigned char m_red;
    unsigned char m_green;
    unsigned char m_blue;
};

どちらの方法もよく使われています。新C++編では今後、データメンバに m_ を付加することにします。

コンストラクタについてはほかにも知っておくべきことがありますが、残りの話題は今後のページの中で取り上げていきます。

constメンバ関数

メンバ関数を呼び出す対象になるオブジェクトが const修飾されていることがありえます。この場合、メンバ関数内でデータメンバの値を書き換えられてはいけませんから、const修飾されているオブジェクトからは、データメンバを書き換えない保証がされたメンバ関数だけしか呼び出せないという制約が課せられます。

あるメンバ関数がデータメンバを書き換えていないという保証は、関数本体を注意深く実装するという話ではなく、メンバ関数の宣言にそのマークを明確に付けることで行います。次のように宣言(と定義が別にあれば定義にも)の末尾に constキーワードを付加します。

class クラス名 {
    戻り値の型 メンバ関数名(仮引数) const;
};

戻り値の型 クラス名::メンバ関数名(仮引数) const
{
    本体
}

このように const が付加されたメンバ関数は、constメンバ関数 (const member function) と呼ばれます。

constメンバ関数の本体内で、データメンバを書き換えるコードを記述すると、コンパイルエラーとして検出されます。また、constメンバ関数から、const でないメンバ関数を呼び出すこともできません。

実際には、後述する thisポインタが constポインタになることで、constポインタ経由の書き換えになり、コンパイルエラーとなります。

#include <iostream>

class C {
public:
    void f1(int v)
    {
        m_value = v;
    }
    void f2(int v) const
    {
        m_value = v;  // コンパイルエラー。constメンバ関数内でデータメンバを書き換えられない
    }

private:
    int m_value;
};

int main()
{
    C a1 {};
    const C a2 {};

    a1.f1(10);
    a1.f2(10);  // OK. const修飾されていないオブジェクトが、constメンバ関数を呼ぶことは問題ない
    a2.f1(10);  // コンパイルエラー。const修飾されたオブジェクトは、constメンバ関数でなければ呼び出せない
    a2.f2(10);  // OK. constメンバ関数は呼び出せる
}

const修飾されたオブジェクトであっても、データメンバの値や、そこから計算された何かしらの結果を取得する必要性があるのなら、constメンバ関数によって提供するようにします。Colorクラスでいえば、色の値を取得する必要性はあるはずですから、get_red、get_green、get_blue といったメンバ関数は、constメンバ関数にするのが適切です。

class Color {
public:
    unsigned char get_red() const
    {
        return m_red;
    }

    unsigned char get_green() const
    {
        return m_green;
    }

    unsigned char get_blue() const
    {
        return m_blue;
    }
};

なお、コンストラクタは、const修飾されたオブジェクトでも呼び出されます。const版のコンストラクタのようなものは存在しません。

thisポインタ

コンストラクタの項で登場した thisポインタ は、this というキーワードで表現される特殊なポインタで、メンバ関数の本体でだけ使用できます。thisポインタは、メンバ関数を呼び出す元になったオブジェクトを指し示しています。したがって thisポインタの型は、自身のクラスのポインタ型です。

C a {};
C b {};

a.f();  // fメンバ関数の本体内で、thisポインタは a を指し示す。型は C*
b.f();  // fメンバ関数の本体内で、thisポインタは b を指し示す。型は C*

オブジェクトを指し示すポインタを使って呼び出している場合も、thisポインタが指し示すものは、あくまでオブジェクトそのものです。

C a {};
C* p {&a};
p->f();  // fメンバ関数の本体内で、thisポインタは a を指し示す。型は C*

メンバ関数から、ほかのメンバ関数を呼び出しても、thisポインタが指し示すものは変わりません。thisポインタの値を出力して確認してみます。

#include <iostream>

class C {
public:
    void f1()
    {
        std::cout << this << "\n";
        f2();
    }

    void f2()
    {
        std::cout << this << "\n";
    }
};

int main()
{
    C a {};
    std::cout << &a << "\n";

    a.f1();
}

実行結果:

0099FEC3
0099FEC3
0099FEC3

メンバ関数が constメンバ関数の場合は、thisポインタが constポインタ(「配列とポインタ」のページを参照)になります。これにより、データメンバの値を書き換える行為が、きちんとコンパイルエラーとして検出されます。

class C {
public:
    void f(int v) const
    {
        this->m_value = v;  // コンパイルエラー。this は constポインタだから、指し示す先の値を書き換えられない
        m_value = v;        // コンパイルエラー。this-> が省略されているだけなので、上の行と同じ理由
    }

private:
    int m_value;
};

【上級】このため、const_cast によって thisポインタの const性を取り外すことで、constメンバ関数内であっても、データメンバの値を書き換えるコードを記述できます。しかし、thisポインタが指し示す先にあるものが const なのであれば、実際にデータメンバの値を書き換える行為は未定義の動作であって、危険であることに変わりはありません。

ラムダ式と thisポインタ

ラムダ式の簡易キャプチャやデフォルトキャプチャの対象は、(静的ローカル変数を除く)ローカル変数と仮引数であり(「関数ポインタとラムダ式」のページを参照)、データメンバはキャプチャできませんが、初期化キャプチャを使って [value = this->m_value] のようなかたちでキャプチャすることは可能です。

thisポインタはキャプチャできます[this] でコピーキャプチャされますし、[=][&] といったデフォルトキャプチャの対象にも含まれます。ここで、デフォルトキャプチャで this がキャプチャされることが、少々誤解を生むことがあります。

#include <iostream>

class C {
public:
    void set_value(int v)
    {
        auto f = [=]() {    // デフォルトキャプチャ時に this もキャプチャされる。

            // データメンバはキャプチャできないので、m_value は使えないはずだが、
            // this がキャプチャされたため、this->m_value としてアクセスできる。
            m_value = v;
        };
        f();
    }

    int get_value() const
    {
        return m_value;
    }

private:
    int m_value;
};

int main()
{
    C c {};
    c.set_value(999);
    std::cout << c.get_value() << "\n";
}

実行結果:

999

ラムダ式の本体からアクセスできないはずのメンバ変数m_value が使えています。キャプチャの指示は [=] なので、コピーキャプチャされたかのようにみえますが、キャプチャされたのは m_value ではなく this の方です。m_value = vthis->m_value = v を意味しており、コピーされた m_value ではなく、本物の m_value を書き換える結果になります。

キャプチャされた this は、キャプチャが行われたときの this をコピーしたものであることに注意が必要です。コピーされているものはポインタであって、指し示している先にあるオブジェクトではありませんから、使い方によっては、オブジェクトのほうが先に寿命を終える恐れがあります。寿命が尽きたオブジェクトを間接参照してしまえば、当然、未定義の動作ということになります。この問題を避けるためには、キャプチャしたいデータメンバを一旦コピーして、そちらをキャプチャすることが考えられます。

void C::f()
{
    auto value = m_value;  // 一旦、ローカル変数にコピー
    [value]() {  // コピーのほうをキャプチャ
        // ...

        // this をキャプチャしていないし、ラムダ式の本体からデータメンバにはアクセスできないので、
        // this が指し示していたオブジェクトが先に消えたとしても問題ない。
    };
}

初期化キャプチャを使うと、同じことをもう少し簡潔に書けます。

void C::f()
{
    [value = m_value]() {  // m_value をコピーキャプチャ
        // ...
    };
}

【C++17】[*this] というキャプチャが可能になりました2。このキャプチャでは、ラムダ式を評価した時点で this が指し示しているオブジェクトをキャプチャします。

【C++20】[=, this] というキャプチャが可能になりました3。これは [=] とだけ書くことと同じですが、this がキャプチャされていることを明確に示せるという意味があります。

メンバ関数ポインタ

関数ポインタとラムダ式」のページで関数ポインタを紹介しました。同じ考え方でメンバ関数を指し示す関数ポインタを作れそうに思えますが、メンバ関数を起動するには、対象となるオブジェクトの存在が必要です。そのため、通常の関数ポインタの仕組みや文法のままというわけにはいきません。

そこで、「構造体とポインタ」のページで紹介したメンバポインタを使います。「構造体とポインタ」のページでは、データメンバを指し示す方法として紹介しましたが、同じ考え方でメンバ関数を指し示すようにします。このようなメンバポインタは、メンバ関数ポインタ と呼ばれます。

【上級】対象のメンバ関数が静的メンバ関数の場合は、対象のオブジェクトが存在しないので、通常の関数ポインタとして実現できます。逆に、メンバ関数ポインタでは指し示すことができません。

メンバ関数ポインタを宣言する構文は以下のとおりです。

戻り値の型 (クラス名::* 識別子)(仮引数の並び) 初期化子;
戻り値の型 (クラス名::* 識別子)(仮引数の並び);

「仮引数の並び」は、引数がないなら空で構いません。また、仮引数の名前は使わないので省略できます。

「初期化子」に指定するのは、「クラス名」に指定したクラスのメンバ関数のメモリアドレスです。仮引数や戻り値が一致していなければなりません。メンバ関数のメモリアドレスは、&クラス名::メンバ関数名 で取得できます。

class C {
public:
    void f1(int x)
    {
    }

    void f2()
    {
    }
};

int main()
{
    void (C::* pf)(int) {&C::f1};  // OK
//  void (C::* pf)(int) {&C::f2};  // コンパイルエラー。C::f2 の仮引数が (int) でない
}

いったん型の別名を定義するのなら、次のようになります。

using func_ptr = void (C::*)(int);
func_ptr pf {&C::f1};

メンバ関数ポインタを経由して、指し示す先にあるメンバ関数を呼び出すときは、オブジェクトの存在が必要です。

(オブジェクト.*メンバ関数ポインタ)(実引数の並び)
(オブジェクトを指し示すポインタ->*メンバ関数ポインタ)(実引数の並び)

具体的には次のようなコードになります。pf に格納している内容を変更すれば、同じ呼び出しのコードが、別のメンバ関数を呼び出すようになります。もちろん、pf がヌルポインタの場合は未定義の動作です。

C c {};
(c.*pf)(100);

C* p {&c};
(p->*pf)(100);

std::function を使う場合は、対象のオブジェクトを引数で渡すようにします。constメンバ関数を使う場合には対象のオブジェクトが const でなければならないので、std::function に指定する仮引数に const を付加します。

#include <functional>
#include <iostream>

class C {
public:
    C(int value)
    {
        m_value = value;
    }

    void add(int x)
    {
        m_value += x;
    }

    void mul(int x)
    {
        m_value *= x;
    }

    void print() const
    {
        std::cout << m_value << "\n";
    }

private:
    int m_value;
};

int main()
{
    C c(100);
    std::function<void (C&, int)> calc_func {&C::add};
    calc_func(c, 5);
    calc_func = &C::mul;
    calc_func(c, 5);
    c.print();

    const C c2(100);
    std::function<void (const C&)> print_func {&C::print};
    print_func(c2);
}

実行結果:

525
100

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のプログラムで、コンパイルエラーになる箇所を挙げてください。

#include <iostream>

class C {
public:
    C(int value)
    {
        m_value = value;
    }

    void set_value1(int value)
    {
        m_value = value;
    }
    void set_value2(int value) const
    {
        m_value = value;
    }

    int get_value1()
    {
        return m_value;
    }
    int get_value2() const
    {
        return m_value;
    }

private:
    int m_value;
};

int main()
{
    C c1(100);
    c1.m_value = 200;
    c1.set_value1(300);
    c1.set_value2(400);
    std::cout << c1.get_value1() << "\n";
    std::cout << c1.get_value2() << "\n";

    const C c2(100);
    c2.m_value = 200;
    c2.set_value1(300);
    c2.set_value2(400);
    std::cout << c2.get_value1() << "\n";
    std::cout << c2.get_value2() << "\n";
}

解答・解説

問題2 (基本★★)

ペイントスクリプトの penコマンドを実装することを想定して、ペンを表現したクラスを作成してください。

penコマンドは、たとえば「pen 255 0 0」のように RGB の強さとともに使用し、以降の描画コマンドで使う色が変更されるというものです。現在の色を管理するために、Penクラスを使います。

解答・解説

問題3 (応用★★)

ペイントスクリプトで使えるように、以下のメンバ関数を備えた Canvasクラスを作成してください。

  1. 縦横のピクセル数を指定できるコンストラクタ
  2. キャンバス全体を指定色で塗りつぶす
  3. 指定した位置に、ペンで点を描く
  4. 指定した範囲に、ペンで四角形の枠を描く
  5. 4の関数を、ペンで内側を塗りつぶすようにしたバージョン

解答・解説


解答・解説ページの先頭



更新履歴




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