2002-12-11 103295

仮想関数への関数ポインタ

長い識別子で書いたように、関数ポインタを受けるコンストラクタでクラッシュする件なのですが、少しいじったら再発するようになってしまいました。で、色々と調べたところ識別子の長さは関係なさそうです。小さなプログラムで再現させられていないのでまだ不確かなのですが、前方参照宣言しかしていないクラスの仮想メンバ関数のポインタを受け取ると問題が起きるような感じです。ただそれだけではなく、実際のクラスが多重継承しているのも何かの関係があるようでもあります。通常のメンバ関数へのポインタと仮想関数への関数ポインタは形式が違いますが、さらにサイズも違ったりしていて、前方参照だけだと通常のメンバへのポインタとみなして計算してしまうというような感じかもしれません。

いまいち再現条件がはっきりわからないのですが、前方参照宣言だけでなくクラスの宣言自体を関数ポインタの宣言の前に持ってくれば大丈夫なようです。なので、ヘッダ側で#includeすることで対処しておきました。

メンバ関数へのポインタ

色々調べてみたところわかりました。そもそもVCでは、メンバへのポインタを効率よく動かすためにこしゃくな^^;最適化を行っているようです。なので、以下でメンバ関数へのポインタのサイズが異なります。

struct A;                  // 前方参照のみ
typedef void (A::*PFN)();  // sizeof(PFN)は16
struct A {                 // 完全なクラス宣言
  void foo();
};
typedef void (A::*PFN)();  // sizeof(PFN)は4

なので、同じ関数ポインタでも、クラス定義がされているかどうかで形式が異なり、サイズも違います。なので、二つの翻訳単位でこの関数ポインタを宣言して、片方では前方参照のみ、片方では完全なクラス宣言という状態になるとサイズが違うオブジェクトを生成することになり、メモリを破壊してしまいます。

この辺の話は、継承を使ったクラスのメンバを指すポインタの処理形式に詳しく説明されていて、コンパイルオプションか#pragma、継承タイプの宣言を使うことで回避できるようです。ただし、そのようにすると効率が悪くなるということなので、可能ならば関数ポインタの宣言の前にクラス宣言をするようにしてしまった方が良さそうですね。しかし、警告くらい出してくれても良さそうなものですね。