Aug 14, 2007

忘れやすいC++の仕様(3/3)

1つ前のエントリーの続きである。

(5) 純粋仮想関数の定義方法
クラス定義内で、"virtual void foo() = 0;"のように、メソッドのプロトタイプにて、関数=0;とする。
関数の宣言と同時に、関数のポインタの値を0に決めてしまうという感じだろうか。独特の風味だ。

この"=0"を外すと、派生クラスに関数の実体があっても、基底クラスの仮想関数の実体が無いということでコンパイルエラーになるし、"=0"じゃなく"{}"とすると、その基底クラス(仮想クラス)のインスタンスを作ることが可能になってしまう。

(6) デストラクタはvirtualにすると派生クラスのデストラクタも呼ばれる

class CLS{
    …
public:
    virtual ~CLS() {…}  /* (g) */
};

class DRV : public CLS{
    …
public:
    ~DRV() {…}
};

int main()
{
    CLS* p = new DRV;
    delete p;  /* (h) */
}
このコードにおいて、(h)の基底クラスのポインタを使ったdeleteを実行すると、派生クラスのデストラクタDRV::~DRV()と基底クラスのデストラクタCLS::=~CLS()が順に呼び出される。もし(g)のvirtualが無いと、CLS::=~CLS()しか呼び出されない。
従って、基底クラスのデストラクタはvirtualにすべしと言えるだろう。

蛇足だが、派生クラスのポインタを使ってdeleteすると、基底クラスのデストラクタをvirtualにしていなくても、派生クラスのデストラクタの後に基底クラスのデストラクタが呼び出される。

(7) フレンド関数の1つ目の引数は型変換される
フレンド関数の1つ目の引数については、フレンド関数の他に適用できるクラスメソッドや関数やが無く、型変換により適用可能であれば、自動的に型変換されて適用される。
クラス外で定義されるクラスメソッドは、1つ目の引数が型変換されて適用されることは無い。従って、フレンド関数はクラスメソッドよりも適用範囲が広いと言える。

次のコードは、1つの演算に適用できるクラスメソッドとフレンド関数との両方がある例である。

class CLS1{
    friend class CLS2;
    int i;
public:
    CLS1(int i=0){this->i = i;}
    CLS1& operator+(double d){i += (int)d; return *this;}  /* (k) */
};

class CLS2{
    double d;
public:
    CLS2(){d = 0.0;}
    CLS2(CLS1 &i){d = (double)i.i;}  /* (l) */
    friend CLS2& operator+(CLS2, double);  /* (m) */
    double get() const {return d;}
};

CLS2& operator+(CLS2 c2, double e)  /* (m) */
{
    c2.d += e;
    return c2;
}

int main()
{
    CLS1 c1(3);
    CLS2 c2 = c1 + 2.5;  /* (n) */
}
このコードにおいては、(n)の演算において(k)のクラスメソッドがc1.operator+(2.5)という形で呼び出され、演算結果は5.0になるが、もし(k)の行が存在しないと、(n)の演算において、(l)による型変換が自動的に適用されて、(m)のフレンド関数がoperator+(CLS2(c1), 2.5)という形で呼ばれ、演算結果は5.5になる。

Posted at 11:05 in PC一般 | WriteBacks (0)
WriteBacks

Aug 13, 2007

忘れやすいC++の仕様(2/3)

続いて、C++のクラスの仕様に関する、忘れていたことを書き留める。

(1) 定数オブジェクトは定数メソッドしか使えない
例:

class CLS{
    int i;
public:
    CLS(int i = 1){this->i = i;}
    int get() const {return i;}  /* (a) */
    void set(int i) {this->i = i;}
};

int main()
{
    CLS c1;
    const CLS c2(2);
    int i;

    c1.set(3);
    c2.set(4);    /* (b) */
    i = c1.get();
    i = c2.get();    /* (c) */
}
このソースをコンパイルすると、(b)でエラーになる。
もし(a)の行のconstを外すと、(c)もコンパイルエラーになる。コンパイル時にreadonlyとwritableの区別はメソッド呼び出し毎に厳密になされる言語仕様らしい。
定数オブジェクトが出現する可能性を考えると、クラス定義においてメソッドの定数定義は重要だ。

(2)デフォルトコンストラクタが無いと配列を定義できない

class CLS{
    int i;
public:
    CLS(int i){this->i = i;}    /* (d) */
};

int main()
{
    CLS c[2];    /* (e) */
}
このコードは、(e)がコンパイルエラーになる。
C++では、配列の定義時に引数のあるコンストラクタが呼べないので、引数なしで呼べるコンストラクタ(デフォルトコンストラクタ)が存在しないと、そのクラスの配列は定義できない。
従って、CLS::CLS()やCLS::CLS(int i=0)のようなメソッドが必要になる。

なお、このコードの(d)を削除すると、コンパイルが通る。明示的なコンストラクタ定義が1つも無ければ、自動的にデフォルトコンストラクタが作成されるからだ。

(3)単項演算子の定義方法
単項演算子には前置(++obj等)と後置(obj++等)があり、それぞれ定義方法が異なる。
クラス名をCLS、演算子を@とすると、前置はCLS::operator@() / operator@(CLS&)、後置はCLS::operator@(int) / operator@(CLS&, int)で定義する。呼び出し時には、int引数には0が入る。

また、単項演算子関数が返す型は、CLSでもCLS&でも良い。だから、通常は異なる値を生成する(一時的なインスタンスを作って返す)マイナス演算子が何もしないなら、

CLS& CLS::operator-() {return *this;}
と参照を返すこともできる。

(4) コピーコンストラクタと代入演算子の共通化
コピーコンストラクタを定義する必要がある場合、代入演算子もそっくりな内容で定義する必要がある場合が多い。
そんな代入演算子を定義しようとして、

class CLS{
private:
    …
public:
    CLS(const CLS& c){…}
    const CLS& operator=(const CLS& c){CLS(c); return *this;}  /* (f) */
};
のようにコピーコンストラクタを普通のメソッドと同じ感覚で呼び出そうとすると、コンパイルエラーになる。なぜなら、上記の"CLS(c);"は"CLS c;"と同じ意味(!)、つまり新たなインスタンスの定義になるからだ。
コピーコンストラクタと代入演算子の処理を共通化するには、次のように別な関数を用意する必要がある。
class CLS{
private:
    …
    inline void copy(const CLS& c){…}
public:
    CLS(const CLS& c){copy(c);}
    const CLS& operator=(const CLS& c){copy(c); return *this;}
};

(参考文献:JIS X 3014)

Posted at 22:13 in PC一般 | WriteBacks (0)
WriteBacks

Aug 10, 2007

忘れやすいC++の仕様(1/3)

私は10年ほど前にC++の仕様を隅から隅まで覚えたことがあるが、それから10年、全く触っていない。Cは時々接するのでANSI Cの仕様はまだ結構覚えているが、C++の仕様は全般的にうろ覚えだ。

そんな状態で、久々にC++の仕様を眺めていると、すっかり忘れていることがいくつもあったので、ここに記す。

(1) 構造体内の構造体定義の参照


struct Outer{
struct Inner{
int i;
} j;
}

というコードのInnerを参照する場合、Cだと

struct Inner i;

とできるが、C++だと

struct Outer::Inner i;

としないといけない。

(2)int値のenum型への変換

Cでは


enum Color {RED=1, GREEN, BLUE};
enum Color c;
c = 1;

ができるが、C++ではできない。

c = Color(1);
c = (Color)1;

のようにする必要がある。

蛇足だが、Cで"c = Color(1);"とすると、コンパイルエラーとはならず、Color()という関数が無いというリンクエラーになる。

(3)template文はクラス定義以外にも使える


template<class T> T square(T x)
{
T y;
y = x * x;
return y;
}

のような関数定義もできる。

ひょっとしてtemplateをクラス定義にしか使えないと思い込んでいる人は少数だろうか。

(4)通常型引数と参照型引数の多重定義

C++では引数の型を変えて同じ名前の関数を多重定義できるが、


void foo(int i) {}
void foo(int &i) {}

の2つを同時に定義することはできない。

よく考えると、これらの関数を呼び分けられないので当然とも思える。

同様に

void foo(int i) {}
void foo(const int &i) {}

の組み合わせも不可だが、

void foo(int &i) {}
void foo(const int &i) {}

の2つは同時に定義できる。

なお、g++では重複定義自体はエラーにならず、呼び出し文にてコンパイルエラーとなった。


See more ...

Posted at 21:23 in PC一般 | WriteBacks (5)
WriteBacks

C++の複雑な仕様の中をよく覚えてますね! まねできないっす。。。 最近はCさえ忘れてきました。。。 もっぱらjavaで、ぬるま湯につかってます。

Posted by KaBA at 08/12/2007 11:42:34 AM

いえいえ、すっかり忘れてたってことでして… どうでもいいですが、MovableTypeでソースコードを貼り付けるの難しいですね。preタグを使うとやたら行間が空くし、preタグを使わずにインデントを表示しようとしたら全角スペース使うくらいしかないし、そうするとcopy&pasteでコンパイルエラーになるソースになるし… preタグ中でも"<"や">"が消えてしまうのは盲点でした。

Posted by ynomura at 08/13/2007 10:04:03 PM

C++ってホント難しいですね。 私は多重継承+friend関数で脱落しました。 でも、いろいろと応用が効きそうですね。 ynomuraさんの記事をみると。 あとは、interfaceが使えるといいのですけれど。今はどうなんですか? 演算子オーバーロードは初心者の頃、惹かれました。 私もHTMLはとっくの昔に忘れてしまいました。 pタグか、quote(?)タグはどうですか? あ、インデントか。。。 ネストもできなかったでしたっけ? <>は要注意っすね。

Posted by KaBA at 08/14/2007 08:36:47 PM

今回実は大学時代に取ったC++の入門書のコピーを捨てる前に読み返してたんですが、読む前は、friendって何で要るんだったっけ?という状態でした。特に演算子のオーバーライドをする時は重要のようです。C++はJavaと違って多重継承ができるので、インターフェースは昔から抽象クラス(仮想クラス)として記述することになってます。

class XXX_Interface{public:    virtual void xxx() = 0;};class YYY_Interface{(略)};class The_Implementer :  The_Superclass, XXX_Interface, YYY_Interface{(略)};
みたいに。HTMLでのインデントについては、結局HTML 4.01の仕様を調べたんですが、下記URLの9.1節に、preタグ以外では連続した空白は単語の区切りと解釈する、と明記されています。しかもTabは強く非推奨(9.3.4節)だそうで。http://www.w3.org/TR/html401/struct/text.htmlネストは可能なんですが、<pre>を使った途端に行間が空いてしまいます。使ってるstylesheetのせいかも知れませんが。というか、codeタグ(9.2節)を使ってもインデントが無視されてしまう仕様なのが、実に不思議です。

Posted by ynomura at 08/16/2007 08:49:18 AM

preタグを使うとやたら行間が空くのは、stylesheetのせいではなく、エントリーに「改行を変換する」(Convert Line Breaks)設定をした場合のMovableTypeの仕様だったようで、<pre>~</pre>の間の改行にも<br />が追加されて改行が2重になってたのが原因でした。<pre>~</pre>等の間の改行は変換しないプラグインもあるようですが、<pre>や<table>を使うエントリーは「改行を変換する」設定にしない方が良さそうです。

Posted by ynomura at 02/12/2008 11:28:33 PM