汎用ポインタへのポインタを受け取る関数

汎用ポインタについて某師から教えてもらったのですが、分かったような分からないような感じなので、今の理解を書いてみます。

(注)以下の記述は間違いを含む可能性があるため、疑いの目で読んでくださいますよう、お願いします。

サンプルコードと疑問

次の関数について考えます。

void f(const void** data);
なんだこりゃ
  • f関数にはどのように値を渡せばよいのか?
  • constの付いた仮引数は、「どこまで」値が変更されないのか?
    • ポインタが変更されないのか? ポイントされるものが変更されないのか?

constについて、「仮引数にconstが付くとき、渡された値は変化しない」という捉え方をしていたために、理解に苦しみました。

また、f関数の戻り値はvoidな上に、いわゆるconst参照渡しではないにもかかわらず、f関数の呼出し後に結果を利用できるということも、混乱に拍車をかけました。

(Photo by id:fumokkm

使い方・考え方

f関数の使い方

f関数は、こうして使います。

#include <stdio.h>
#include <iostream>

void f(const void** data)
{
    std::string s = "hoge";
    std::string* ps = &s;
    *data = &ps;    // (3)
    
    std::cout << &ps << '\n';
}

int main()
{
    const void* out;    // (1)
    f(&out);    // (2), (4)
    
    std::cout << out << '\n';
}

(2011/12/13 追記) f関数の中で仮引数にローカル変数を代入していますが、値p, spはf関数のスコープを外れると無効になるため、実用上はよくないコードです。dataに値を代入できることを確認するために示しました(もう少し良い例に直したいです。すみません)。tem_masさんからコメントでご指摘いただきました。ありがとうございます。

f関数の引数は、「汎用ポインタへのポインタ」です。汎用ポインタとは、どんな型のポインタでも受け取ることができるポインタです。(JavaのクラスでいうところのObject型によく例えられます)

f関数の役割は、渡された汎用ポインタを利用可能な状態にして、呼出し元に返すことです。

つまり、上のコードでやっているのは、次のようなことです。

  1. 呼出し元で汎用ポインタを宣言する
    • この時点では、何もポイントしていないポインタが宣言される(だけ)
  2. f関数に汎用ポインタのアドレスを渡す
    • このアドレスは、決して変更されない(※)
  3. f関数内で、仮引数の実体である汎用ポインタが、「何か」をポイントするように、値を代入する
  4. 呼び出し元に戻った後、ポイントした「何か」が利用可能になる

(※)汎用ポインタが格納されているアドレスが変更されていないことは、main関数とf関数それぞれの末尾にある出力によって、確認できます。同じアドレス値が出力されます。

図であらわす

(1)がf関数の呼出し前、(2)が呼出し後です。

汎用ポインタ自体のアドレスは、f関数の呼出し前と後とで、変わりません。# 図中のアドレス値はテキトーです。

ポインタは、f関数の内部で、「hoge」なる文字列の先頭アドレスをポイントするようになりました。

出力引数

これは、C#でいうと、outキーワードを使った出力引数に当たるそうです。

参照渡しを使うと、メソッド内でオブジェクトを初期化することが出来るようになります。 つまり、呼び出し元では変数に意味のある値を格納せず、 メソッド内で値を代入してもらうことで変数の初期化を行うことが出来ます。

http://ufcpp.net/study/csharp/sp_ref.html

C++の解説が読みたいのですが、どんな単語でググればよいのか、よく分かりません・・・。

実際のコード例

現実のプロダクトのコードで、このイディオムが使われる例を見てみます。…というか、順序としては逆で、これを見ていて、「なんぞこれ」という話になったのでした。

出典は、Protocol Buffersです。

constがついた汎用ポインタを仮引数に持つ関数

http://code.google.com/p/protobuf/source/browse/trunk/src/google/protobuf/io/zero_copy_stream_impl.h#225

  // implements ZeroCopyInputStream ----------------------------------
  bool Next(const void** data, int* size);

使う側のコードは、こんなかんじ。

http://code.google.com/p/protobuf/source/browse/trunk/src/google/protobuf/io/zero_copy_stream_unittest.cc#163

  const void* in;    // const付きで汎用ポインタを宣言
  
  // (略)
  
  while (true) {
    if (!input->Next(&in, &in_size)) {    // Next関数呼出し
      return size - out_size;
    }
    // (略)
    if (out_size <= in_size) {
      memcpy(out, in, out_size);    // 結果の利用
      // (略)
      return size;  // Copied all of it.
    }
constなしの汎用ポインタを仮引数に持つ関数

http://code.google.com/p/protobuf/source/browse/trunk/src/google/protobuf/io/zero_copy_stream_impl.h#269

  // implements ZeroCopyOutputStream ---------------------------------
  bool Next(void** data, int* size);

使う側のコード。

  void* out;    // constなしで汎用ポインタを宣言
  // (略)
  
  while (true) {
    if (!output->Next(&out, &out_size)) {    // Next関数呼出し
      return false;
    }
    // (略)
    memcpy(out, in, out_size);    // 結果の利用
    // (略)
  }

参考

素っ頓狂な顔をしたネコの写真は、ふも氏(id:fumokkm)から頂戴しました。ありがとうございます!