2017-12-25

Cプログラマのための、C++簡単裏道アプローチ(2)

ポインタより便利な参照型

最近、純粋C言語を22年ぶりに使ってとても不便に感じたことの1つは、CにはC++の参照型がないことでした。

Cでは参照型に似たものにポインタ型があります。
しかし、関数のパラメータの受け渡しの多くでNULLチェックをしなければならず、無駄なコードが量産される結果となってしまいます。

ポインタ型と参照型、関数の引数ではどちらも一般的な用語で参照渡しと呼ばれるものになります。コンパイルされた結果は差異がないか、区別できないかも知れません。
では、何が違うのか。決定的に違うのは、ポインタ型はアドレス値そのものを値として取り出したり、変更したり、演算したりできますが、参照型はそれを値として取り出したり操作したりできないのです。

このことは、ポインタ型にはない重要な側面があって、NULLポインタという状態になり得ないので、いちいちNULLチェックをする必要がないのです。NULLではないことが保証されています。

コード1

void func1(int * pArg1, int & rArg2)
{
    if(pArg1 == NULL) exit(1);

    // 参照型に特別なアクセス方法はない
    printf("arg1 = %d,  arg2 = %d/n", * pArg1, rArg2);
}

int main()
{
    int v1 = 1;
    int v2 = 2;

    func1( &v1, v2 );

    return 0;
} 

このコード例では func1()の2番目の仮引数に&(アンパサンド)が付いています。これが参照型の宣言です。

では、これを呼出すところでコード2のようにしたらどうでしょうか。

コード2

int main()
{
    func1( NULL, NULL );

    return 0;
} 

これではコンパイルエラーになってしまいます。参照型にNULLを指定していますが、参照型は値そのものではなく(NULLも値そのもの)、変数の実体を渡す必要があるのです。

では、参照型の変数を初期化しなければどうなってしまうのでしょうか。

コード3

void func2()
{
    int & v1;
}

これもコンパイルエラー。参照型は、常に何かの変数を参照していなければなりません。
ただし、注意しなければならないパターンがあります。関数の戻り値を参照型にする場合です。これは、ポインタの場合でも同様に問題ですが、参照型の場合でもこれがコンパイル可能になってしまうからです。

コード4

int & func2()   // int の参照型を返す
{
    int v1 = 1;
    return v1;
}

これはコンパイル時はwarning 扱いにはなりますが(g++ 6.4.0で確認)、コンパイルは通ります。しかし、実行時エラーになってしまうのです。

const キーワードを使うとポインタ先の内容を書き換えられないようにできます。参照型も同様です。const型の参照、ということになるからです。

コード5

void func3(int & a1, const int & a2)
{
    a1 = 10;    // 参照先を書き換える
    a2 = 20;    // コンパイルエラー
}

ところで、これだったらわざわざa2を参照型にする必要はないのではないでしょうか?
その通りです。int型ならば値渡しで構いません。むしろその方がコンパイル結果のコードに無駄が少なくなることが期待できます。
constを使用したリードオンリーの参照型を使用する場合、大きな実体を持つオブジェクト(C言語なら構造体が相当します)で特に意味を持ちます。
つまり、値渡しによって大量のデータをコピーする無駄を省くことができるからです。
今まではそういった場合、const ポインタを使用していたと思います。しかし、constの参照型も同様の理由で使用することができます。

コード6

struct BigData               // typedefが不要な点に注意
{
    int  a, b;
    long data[1024];
};

void func4(BigData  d1, const BigData & d2)
{
    printf("d1.a = %d, d2.b = %d, d1.a, d2.b );
}

仮引数のd1は値渡しなのでデータが全てコピーされて比較的処理が重くなります。d2は参照型による参照渡しですから、NULLポインタの心配をせずに同様の処理が可能だし、func4()で中身が書き換えられてしまう心配もありません。
Cではこんな場合でもポインタを使用せざるを得ませんが、やはりNULLの問題は避けられません。

以上のようなわけで、C++ではポインタの出番はとても少なくなります。
逆にポインタが必要な場合はどんな場合でしょうか。
これは読者への課題ということにしましょう。




コラム:値渡しと参照渡しの話


値渡しと参照渡しについては何となく理解して頂けたでしょうか。C++の場合、参照渡しはポインタ型と参照型で行われます。
しかし、最近の高級言語ではこれが区別なく行われることが多くなっています。

C#もその1つです。個人的にはC#はとても素晴らしい言語であると感じていますが、やはり値渡しなのか参照渡しなのかを表面上は区別しません。
プログラマに余計な事は考えさせず、直感的なコーディングが行えるようにしているのかも知れません。しかし、値渡しなのか参照渡しなのかをまったく意識せずにプログラミングが可能なのか?といえば、そうではありません。結局、多くの場合でそれを意識しなければならないのです。

C#では構造体に相当するクラスが、定義する時点で値渡しにするのか、参照渡しにするのか区別するようになっていますが、実際のオブジェクト(実体)だけを見て区別することはできません。これは自分で定義したクラスだけではなく、多くの標準的なライブラリにおいても同様です。これはけっこう困ることがあります。

これはC#の最大の欠点であり、致命的なもののようにも思えます(この点だけを改良した同じような言語を別に作って欲しいと願いたいくらいです)。言語的には同じC系なのに、何故このような安直なものにしたのか、今となっては後戻りできないでしょうから、大変残念に感じています。

0 件のコメント:

コメントを投稿