C++入門/Class内部の文法による隠蔽

柴田祐樹,東京都立大学,情報科学科
戻る

Class内部の隠蔽・メンバとコンストラクタのページでは、C++のクラス(型)にメンバ関数を割り当て、メンバ参照により関数を呼び出す方法を説明した。メンバ関数により、クラスに対する操作関数の呼び出しのみにより行うことができることを紹介した。関数には制御も含まれているため、通常、変数よりも関数を扱う方が処理の記述が簡単であることから、メンバ関数によりプログラムの記述が容易となる。しかしながら、メンバ変数をクラスの利用者が扱わなくて良くなるということであったが、依然として直接メンバ変数を操作することが可能な状態である。本ページでは、コンパイル時に、メンバ変数をクラス利用時に操作することがないかどうか確認し、操作している場合にコンパイルを中断する(コンパイル時エラーとする)ことで、生成されるコードが確実にメンバ関数のみを使うことを保証するC++の文法規則を説明する。これは、コード作成者の意識でメンバ変数を直接操作しない決まりを維持する方式とは根本的に異なる。そのようなコードを作ることが許されないのである。

本ページでは以下の概念を学ぶ。

publicとprivate

以下のの例ではこれまでになかったprivateとpublicという予約語が記載されている。簡単に言えば、private はこれが記載された以降のクラス内に定義された要素が、メンバ関数以外の演算で参照することを禁止する。publicは逆で、これが記載された以降の行に定義される要素が、メンバ内以外の演算で参照されることを許す。具体的にのコードをコンパイルすると、後に記載ののエラーが得られるはずだ。

 1 #include<iostream>
 2 using namespace std;
 3 struct A{
 4     private:
 5         double x,y,z;
 6     public:
 7         double w;
 8 };
 9 int main(){
10 
11     A a, b;
12     a.x = 2; // NG
13     a.w = 3; // OK
14 
15     b = a; // OK
16     cout << a.x << "\n"; // NG, even not reading.
17     return 0;
18 }
            
メンバ変数とメンバ関数

さて、エラーの読み方をここで学んでみよう。上記access1.cpp:12:7 と記載されている箇所は、例えば、12行目で問題があることを示している。まずは此のように行数だけでいいので、エラーを読めるようになろう。

そして、同様に16行目でも問題が起こっている。今回の場合は、これらの行でわざと問題が起こるようにしてあるので、上記コードは、12行目、16行目を取り除けば動くようになる。15行目の代入演算子は、aの全ての要素をbへ複写するので、aのprivateが指定されたメンバであるx, y, zの値を当然参照するのだが、代入演算子 = は、特別に見逃してもらえるので大丈夫である。

privateの影響を受けるメンバをプライベイトメンバ、publicの影響を受けるメンバをパブリックメンバ、と呼ぶ。教育に悪いのでここではprivateメンバ、publicメンバと書く。

ここで、private メンバは、全く参照できないので、このままでは全く役に立たないから、これを活用する方法を紹介する。private メンバは、publicの関数を呼び出された時に、その呼び出された関数で操作する、というようにするのである。具体的に、へpublicの関数を用いprivateメンバを利用する方法をしめす。

 1 #include<iostream>
 2 using namespace std;
 3 struct A{
 4     private:
 5         double x,y;
 6     public:
 7         A(double x_, double y_){x = x_; y = y_;} // Constructor must be public
 8         void scale(double s){x = x * s; y = y * x;}
 9         double getx(){return x;}
10         double gety(){return y;}
11 };
12 int main(){
13 
14     A a(1, 2); 
15     a.scale(3);
16     cout << a.getx() << ", " << a.gety() << "\n";
17     return 0;
18 }
Privateメンバへのpublic関数内での参照

上記のコードにおいて、x, yはprivateであり、mainの中ではこれらを一度も参照していないが、これらx, yを用いたプログラムとなっている。例えば、コンストラクタにより、x, yの値を設定し、関数scaleで両方の値を3倍にしている。メンバ関数を使うと、複数のprivateの変数をこのように同時に改変することができるため、メンバ関数は、privateのメンバへの単なる検閲という役目のみを担うのではないことがわかる。それよりも、メンバ関数はどのようにメンバ変数を操作すれば目的の状態(ここでは全ての変数をスカラー倍する)へ達することができるか、ということを必要十分(必要最小限という意味)な形で示すため、コーディングの意味がわかりやすい。

16行目の出力部では、getx, getyによりx, yの値を得ている。単に一つの値を得るためだけのこのような関数は記載量が増えて面倒なだけであるが、privateのメンバを読み取るには現状はこのようにするしかない。 もう少し有意義な関数とするならば、

#include<iostream>
using namespace std;
struct A{
    private:
        double x,y;
    public:
        A(double x_, double y_){x = x_; y = y_;}
        void scale(double s){x = x * s; y = y * x;}
        double get(int i){
            if(i%2 == 0) return x;
            else return y;
        }
};
int main(){
    A a(1, 2);
    a.scale(3);
    cout << a.get(0) << ", " << a.get(1) << "\n";
    cout << a.get(2*121) << ", " << a.get(-23121) << "\n";
    return 0;
}
Privateメンバへのpublic関数内での参照

ここで出てきたpublicと private をアクセス指定子と呼ぶ.また,structの代わりにclassを使うことが以下のように可能であるがアクセス指定子の挙動がclassとstructでことなる.classでは何も記載されていないければprivate,逆にstructでは記載がなければpublicであると定義される.はこの挙動の違いを確認できるコードとなっている。実際に、classで定義されたクラスBの変数xはclassが標準でprivateとする決まりから、xを参照とする、16行目でコンパイルが失敗する一方、structで定義されたクラスAにおいては、18行目でxを参照しているわけだが、xはstructの標準のpublicのアクセス指定子を持つこととなっているため、ここでコンパイルの問題が起こることはない。yについてはアクセス指定子がA, Bともに明示された変数であるため、明示的にアクセス指定子に従った挙動を示す。デフォルトのアクセス指定子が違うということであるが,それだけである.初学者はしばらくstructを使いメンバの使い方を学ぶと良いと,著者は思うので先にstructを紹介した。

 1 #include<iostream>
 2 using namespace std;
 3 class B {
 4     double x;
 5     public:
 6     double y;
 7 };
 8 struct A{
 9     double x;
10     private:
11     double y;
12 };
13 int main(){
14     A a;
15     B b;
16     b.x = 1; // NG
17     b.y = 2; // OK 
18     a.x = 3; // OK
19     a.y = 4; // NG
20     return 0;
21 }

演算子とアクセス指定子

演算子を定義する場合のアクセス指定子に関して説明する。ここでは、Classと型で題材とした二次元ベクトルクラスを用いる。まずは、Classと型で紹介されたクラスR2の宣言をstructからclassに変えてコンパイルがどのような結果になるか試してみよう。にこのためのコードを示す。

                 1 #include <iostream>
 2 class R2{
 3     double x;
 4     double y;
 5     R2(double x_, double y_){
 6         x = x_; y = y_;
 7     }
 8     R2(){x = 0; y = 0;}
 9 };
10 R2 operator+(R2 a, R2 b){
11     R2 r;
12     r.x = a.x + b.x;
13     r.y = a.y + b.y;
14     return r;
15 }
16 int main(){
17     R2 a(1, 1.5), b(1, 2.5), c, d;
18     c = a + b;
19     d = R2(1, 1) + R2(-1, 5) + a;
20     std::cout << c.x << ", " << c.y << ", " << d.x << ", " << d.y << "\n";
21     return 0;
22 }
structをclassに変えたクラスR2の例

実行結果は以下のように、privateメンバを参照しようとしているというエラーでいっぱいとなるはずだ。

の実行結果の例

アクセス指定子の課題

それでは、のエラーを解決しつつクラスの内部隠蔽の理念にそったより良いコードを設計してみよう。ただエラーをなくすのではなく、内部隠蔽の理念が現れるように改変したい。演算子があり、演算子内部からクラスのメンバを参照できなければならないので、この点に想像力を必要とするが、具体的には以下のように改変してみよう。

演算子について、2項演算子は基本的にはクラス外部に宣言する必要があるので、クラス側では演算子ではない普通のメンバ関数により、加算を定義する。ただし、メンバ関数では2項演算の片方の引数は、呼び出し元のオブジェクトであるから、引数はそれに加える一つのR2オブジェクトだけとする。

解答例をR2private.cppに示す。

演算子とアクセス指定子2

演算子には,Classのメンバーでしか定義できないもと,クラス外部でも定義できるものがある.2項演算子はほとんどクラス外部で定義できる.以下では2項演算子の例として積*, クラスメンバでしか定義できないものとして添字[]を紹介している.添字は整数でなくても良い.二項演算子では,下記のスカラー倍のような,因子の型が異なる場合があるので,その場合,演算子を定義しているクラスが常に左側に来るとは限らないから,クラス外部に定義したほうが良い.具体的に,以下の定義では,a*2 とかけても,2*aと書くことはできないのである.演算子のメンバ関数は,定義されているクラスのオブジェクトが左に来るという約束があるからである.

#include <iostream>
class A{
    double x0, x1,x2,x3;
    public:
    A(){x0 =-1;x1 = 2; x2 = 4; x3=1.112;};
    double operator[](int i){
        if(i%4==0) return x0;
        else if(i%4==1) return x1;
        else if(i%4==2) return x2;
        else return x3;
    }
    A operator*(double y){
        A r;
        r.x0 = x0 * y;
        r.x1 = x1 * y;
        r.x2 = x2 * y;
        r.x3 = x3 * y;
        return r;
    }
};
int main(){
    using namespace std;
    A a;
    a = a*4;
    cout << a[1] << ", " << a[0] << ", "<< a[3] << ", " << a[-2] << ", " << a[2] << "\n";
    return 0;
}

基礎力課題

を参考に,ベクトルクラスと,回転演算クラスを作成し,2次元の線形代数の式によって星を描いてみよう.class_interface.htmlで説明したSVGクラスを用い,SVGに出力して可視化すると良い.

星を生成するための漸化式.普通の星と違うので注意.

生成した様子は,のようになるだろう.模範解答をstar.ans.cppへ示す.

星の生成例