Entries

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

C++の参照型について、つらつらと

タグ: C++

参照型の特徴は主に以下の2点。

  1. 正当なオブジェクトを参照するよう初期化しなければならないことが規格で明記されている(JIS X 3014:2003 §8.3.2)
  2. 実引数を変更しないことを意味するconst参照型の引数が、値渡しの代替として優秀

1は、端的に言えば「関数の引数型をポインタではなく参照にすることによって、その引数にNULLが渡されないことを確認する責任を呼び出し側に押しつけられる」ということ。それによって、

  • 関数の仕様書から「この引数にNULLを渡してはならない」という記述を省ける
  • 関数の実装から冗長なNULL引数チェックを省ける
  • 関数の呼び出し側は「NULLを渡したらどうなるか」を気にする必要がなくなる

というメリットが生まれる。
これは一種の静的表明(コンパイル前契約)であり、Visual Studio Team Edition for Software Developersにある「コード分析」のようなコンパイル時チェックを支援する。

#include <CodeAnalysis/SourceAnnotations.h>

void reference(int& n)
{
    n = 1;
}

void pointer(int* p)
{
    *p = 1; // VS2005のコード分析では警告せず:NULLの可能性は明白にされていない
}

// 事前条件:引数pはNULLの可能性(Maybe)がある
void nullable([SA_Pre(Null=SA_Maybe)] int* p)
{
    reference(*p); // 警告:NULLの可能性のあるポインタを参照剥がししている
    pointer(p);
}

// 事前条件:引数pはNULLではない(No)
void unnullable([SA_Pre(Null=SA_No)] int* p)
{
    reference(*p);
    pointer(p);
}

void call(bool cond)
{
    int* p = NULL;
    if (cond)
    {
        p = new int;
    }
    reference(*p); // 警告:NULLの可能性のあるポインタを参照剥がししている
    pointer(p); // 問題なし:NULLに関する事前条件はない
    nullable(p); // 問題なし:NULLの可能性があることが事前条件になっている引数にNULLを渡している
    unnullable(p); // 警告:NULLではないことが事前条件となっている引数にNULLを渡している
    delete p;
}
(16) : warning C6011: NULL ポインタ 'p' を逆参照しています: Lines: 16
(34) : warning C6011: NULL ポインタ 'p' を逆参照しています: Lines: 29, 30, 34
(37) : warning C6387: '引数 1' は '0' である可能性があります: この動作は、関数 'unnullable' の指定に従っていません: Lines: 29, 30, 34, 35, 36, 37

この「NULLチェックではないことを保証する義務がどちらにあるかを明確にする」という利点は、関数の戻り値でも活かせる。
たとえば

Hoge& GetAt(int n);

このような関数は、「引数として渡された値が不正であるために正当なオブジェクトを返せない」場合、例外を送出するよう設計するほかない。
よって、異常系はtry-catchに一任し、正常系の流れでは特に成否判定をする必要はない、ということが一目で伝わるのである。
……と、良いのだが。
実際は、例外を使う気もないのにこのような設計をし、結果、失敗時は適当なオブジェクトを返して、別の関数で成否判定を行うという迂遠な設計になっていることもあったりする。

特徴の2番に上げた「値渡しの代替として優秀」は、「値渡し」と「const参照渡し」が

  • NULLオブジェクト不可
  • 実引数を変更しない契約である(厳密に言えばconst参照渡しされた実引数はconst_castで変更可能だが、その行為はgoto以上の禁忌)

点で同義であり、かつconst参照渡しの方が

  • コピーコストが発生しない(ただし参照剥がしのコストは発生するかもしれないので、組み込み型のように「コピーコスト=参照作成コスト」な変数をconst参照渡しするのは得ではない)
  • コピー不可なオブジェクトも渡せる

点で優れているということ。
値渡しでもconst参照渡しでも良い場合、とりあえずconst参照渡しにしておけば問題ない。

この特徴は、しかしそのままconst参照渡しの欠点にもなる。
なまじconst参照渡しが値渡しのほとんどをカバーしているため、const参照型の引数は「値渡しの代替であろう」という先入観を与えてしまう。
たとえば以下の2つの実装。

  1. NULL不可の表明(特徴1)としてconst参照型引数を使用
    template <typename T>
    class Hoge
    {
    public:
        Hoge(const T& ref) : obj(ref) {}
        
        const T& obj;
    }
  2. 値渡しの代替(特徴2)としてconst参照型引数を使用
    template <typename T>
    class Hoge
    {
    public:
        Hoge(const T& ref) : obj(ref) {}
        
        const T obj;
    }

どちらもコンストラクタの設計自体は同じだが、「コンパイルが通れば問題ない(はず)」の2に比べ、1は「コンパイルは通るが実行時に問題になる引数」が多い。

Fuga fuga1;
Hoge<Fuga> hoge(fuga1);
Fuga fuga2 = hoge.obj;

これはどちらの実装でも問題ない。
1の実装でも、3行目では hoge.obj は有効オブジェクト(=fuga1)を指している。

Hoge<Fuga> hoge(Fuga());
Fuga fuga2 = hoge.obj;

1の実装では不可。2行目の hoge.obj が無効オブジェクト(=1行目で生成されて解体された、Fugaの一時オブジェクト)を指すことになる。
関数呼出し内の参照仮引数に結合される一時オブジェクトは、その呼出しを含む完結式の完了まで生存し続けることが保証されている(JIS X 3014:2003 §12.2/5)。逆を言えば、その完結式が完了した次の行まで一時オブジェクトが生存している保証はない。

Hoge<int> hoge(1);
int n = hoge.obj;

定数「1」はずっと生存しているはずだから1の実装でも大丈夫、などと考えたらそれは誤り。
参照は「オブジェクト」か「関数」を指すものであるが、「1」は「リテラル」であり、リテラルはオブジェクトではない(JIS X 3014:2003 §1.8/1)。
よってこちらも一時オブジェクトの生存期間の規定に従い、2行目では hoge.obj は無効オブジェクト(=1行目で生成されて解体された一時変数「1」)を指していることになる。

このような「値渡しの代替ではなく、NULL不可の表明としてconst参照型引数を持つコンストラクタ」は、いろいろ絡み合ってくるとさらにややこしくなる。
たとえばBoost.LambdaBoost.Function
Boost.Lambdaは基本的に「関数オブジェクトとしてテンプレート関数の引数として渡される」ことを想定したものなので、その関数を抜けるまで生存していれば良いからと一時オブジェクトをコピーせず、const参照で保持することがある。
ところが、Boost.Functionはそんなlambdaオブジェクトを一時オブジェクトの生存期間外まで延命する。
そのことを把握せずにこの2つを組み合わせると、時に「既に解体された一時オブジェクトを評価時に参照するコード」を生み出すことになってしまうのである。

そんなわけなので、たとえ「正当なオブジェクトを指していてほしい」引数であっても、「一時オブジェクトを許容できない」ならconst参照型ではなくポインタ型にした方が安全。
非constの参照ならば、一時オブジェクトを受け取れない分const参照より安全だが、それでも「関数を抜けても参照渡しされたオブジェクトへの参照を保持していたい」ならポインタ型にした方が良いかもしれない。

なぜかというと、

  • const参照型引数に一時オブジェクトを渡しているか否か
  • const参照渡しした実引数の生存期間

は「C++固有の警戒事項」であるため問題が正しく把握され難いが、

  • NULLポインタ
  • ポインタ渡ししたオブジェクトの生存期間

は「C及び他言語と共通の危険事項」であるため、警戒してくれる可能性が幾分高いからである。

スポンサーサイト

コメント

コメントの投稿

コメントの投稿
管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://idlysphere.blog66.fc2.com/tb.php/229-c3653440
この記事にトラックバックする(FC2ブログユーザー)

Appendix

タグ

Blog内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。