8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

メンバへのポインタ(C++)

Last updated at Posted at 2019-03-31

メンバへのポインタについてまとまった記事が Qiita に見当たらなかったので、書いてみる。

で。

C言語では

人工的な例でちょっとあれだけど、こんな関数がある:

C言語
void show_sec(){
  time_t timer =time(NULL);
  struct tm *local = localtime(&timer);
  printf( "%d\n", local->tm_sec );
}

void show_min(){
  time_t timer =time(NULL);
  struct tm *local = localtime(&timer);
  printf( "%d\n", local->tm_min );
}

2つの関数の差異は tm_sectm_min かの違いだけ。
ならば、これを引数にしたい。

こんな風に

C言語ではない
void show_xxx(??? member){
  time_t timer =time(NULL);
  struct tm *local = localtime(&timer);
  printf( "%d\n", local->member );
}
void use_show_xxx(void){
  show_xxx(tm_sec)
}

書くことはできないので、こう書いていた:

C言語

void show_xxx( size_t offset ){
  time_t timer =time(NULL);
  struct tm *local = localtime(&timer);
  printf( "%d\n", *(int*)(((char*)local)+offset) );
}

void use_show_xxx(void){
  show_xxx(offsetof(struct tm, tm_sec));
}

offsetof は、構造体の先頭からメンバ変数までの距離が何バイトなのかを計算するマクロ。
なので *(int*)(((char*)local)+offset) という計算が必要になる。

C++ で導入されたメンバへのポインタ

C++ としては、上記のような 醜悪なキャストを受け入れることは到底できない。
そこで導入されたのがメンバへのポインタ。

上記の例を C++ で書くとこうなる:

C++

void show_xxx( int tm::*member ){
  auto timer = std::time(NULL);
  auto local = localtime(&timer);
  std::cout << local->*member << std::endl;
}

void use_show_xxx(void){
  show_xxx(&tm::tm_sec);
}

ご覧のとおり、キャストがない。キャストがないということは型安全。素晴らしい。

実際のところ。以下のコードを実行すると

C++
auto ptr_to_member = &tm::tm_min;
std::cout << *reinterpret_cast<size_t *>(&ptr_to_member) << ", "
          << offsetof(tm, tm_min) << std::endl;

こんな

出力
4, 4

出力を得る。たしかにオフセットが入っている。

対応関係は下表の通り:

言語 オフセットのとり方 オフセットの使い方
C言語 offsetof(struct tm, tm_sec) *(int*)(((char*)local)+offset)
C++ &tm::tm_sec local->*member

offsetof と書いたが、これは実は概ね1マクロ。

こんな

C言語
#define offsetof(type, field) ((size_t)(&((type *)0)->field))

定義になっている。わかる人にはああなるほどという感じかもしれないが、邪悪な印象は否めない。

それと。上記の例では ->* を使っているが、これは左辺が構造体(クラス)へのポインタだったから。左辺が構造体(クラス)そのものの場合は .* を使う。

static メンバは?

前述の通り「メンバへのポインタ」という文法があるのは、C言語の「offsetof」を型安全にするのが目的(だと思っている)。

static メンバについては offsetof がなくても普通のポインタで参照できるので「メンバへのポインタ」は要らないし、使えない。

「クラスのメンバ」には static なメンバも含まれているが、「メンバへのポインタ」という用語における「メンバ」は 非 static なメンバの話に限定される。わかりにくいね。

C++ のメンバには関数もある

C++ の 非 static なメンバには関数もある。

非 static メンバ関数からもメンバへのポインタは取れる。こんな

C++
#include <iostream>

struct foo {
  char const *name;
  void bar(int x) { std::cout << name << " bar: " << x << std::endl; }
  void baz(int x) { std::cout << name << " baz: " << x << std::endl; }
};

void sample() {
  foo abc{ "abc" };
  foo def{ "def" };
  auto mbar = &foo::bar;
  auto mbaz = &foo::baz;
  (abc.*mbar)( 123 ); //=> 「abc bar: 123」を出力
  (def.*mbaz)( 456 ); //=> 「def baz: 456」と出力
}

感じ。

ちなみに、ふつうのメンバへのポインタと異なり、メンバ関数へのポインタは 多くの処理系で 2*sizeof(size_t) になる2

上記の例で何が入っているのかを見ると、手元の clang(x86_64-apple-darwin18.5.0) では

C++
auto mbar = &foo::bar;
auto ary = *reinterpret_cast<std::array<size_t, 2>*>(&mbar);
std::cout << std::hex << ary[0] << ":" << ary[1] << std::endl;
//=>「10d96df20:0」を出力

となった。ary[0] は 関数へのポインタっぽいので呼んでみると:

C++
auto mbar = &foo::bar;
auto ary = *reinterpret_cast<std::array<size_t, 2>*>(&mbar);
typedef void(func_t)(foo*,int);
auto func = reinterpret_cast<func_t*>( ary[0] );
auto f = foo{"hoge"};
func(&f,789); //=>「hoge bar: 789」と出力

ちゃんと呼べる(未定義動作だけど)。

では、0 が入っている ary[1] には何が入っているのか。

C++ のメンバには仮想関数もある。

まずはソースコードと実行例を:

C++
struct base{
  virtual void foo(){ std::cout << "base::foo\n"; }
  void bar(){ std::cout << "base::bar\n"; }
};

struct  derived : public base{
  void foo() override { std::cout << "derived::foo\n"; }
  void bar(){ std::cout << "derived::bar\n"; }
};

void sample(){
  auto m_b_foo = &base::foo;
  auto m_b_bar = &base::bar;
  auto m_d_foo = &derived::foo;
  auto m_d_bar = &derived::bar;

  base b;
  derived d;
  base * b_d = &d;

  (b.*m_b_foo)(); // 「base::foo」を出力
  (b.*m_b_bar)(); // 「base::bar」を出力
  //(b.*m_d_foo)();// コンパイルエラー
  //(b.*m_d_bar)();// コンパイルエラー

  (b_d->*m_b_foo)(); // 「derived::foo」を出力
  (b_d->*m_b_bar)(); // 「base::bar」を出力
  //(b_d->*m_d_foo)();// コンパイルエラー
  //(b_d->*m_d_bar)();// コンパイルエラー

  (d.*m_b_foo)(); // 「derived::foo」を出力
  (d.*m_b_bar)(); // 「base::bar」を出力

  (d.*m_d_foo)(); // 「derived::foo」を出力
  (d.*m_d_bar)(); // 「derived::bar」を出力
}

要するに、普通に 非 static な メンバ関数を呼んでいるのと変わらない動きになる。
当たり前のようにも思うが、ちゃんと作られていて素晴らしい。

で。中身がどうなっているのか見てみると。
こう

C++
struct base{
  virtual void foo(){ std::cout << "base::foo\n"; }
  void bar(){ std::cout << "base::bar\n"; }
};

struct  derived : public base{
  void foo() override { std::cout << "derived::foo\n"; }
  void bar(){ std::cout << "derived::bar\n"; }
};

template <typename klass>
void show_one(char const *name, void (klass::*member)()) {
  auto ary = *reinterpret_cast<std::array<size_t, 2> *>(&member);
  std::cout << name << ": " << std::hex << ary[0] << ":" << ary[1] << std::endl;
}

void show() {
  show_one("&base::foo", &base::foo); // => 「&base::foo: 1:0」を出力
  show_one("&base::bar", &base::bar); // => 「&base::bar: 103cc5c00:0」を出力
  show_one("&derived::foo", &derived::foo); // => 「&derived::foo: 1:0」を出力
  show_one("&derived::bar",
           &derived::bar); // => 「&derived::bar: 103cc5c30:0」を出力
}

なっている。
見てのとおり、仮想関数の場合は ary[0] に仮想関数表の index が入っているっぽい。
しかし依然として ary[1] は未使用になっている。

C++ には多重継承もある

まあまずは例を。

C++
#include <array>
#include <iostream>

struct base0 {
  virtual void bar() { std::cout << "base0::bar\n"; }
};

struct base1 {
  virtual void baz() { std::cout << "base1::baz\n"; }
};

struct base2 {
  virtual void qux() { std::cout << "base2::qux\n"; }
};

struct derived : public base0, public base1, public base2 {
  virtual void foo() { std::cout << "derived::baz\n"; }
};

void show_one(char const *name, void (derived::*member)()) {
  auto ary = *reinterpret_cast<std::array<size_t, 2> *>(&member);
  std::cout << name << ": " << std::hex << ary[0] << ":" << ary[1] << std::endl;
}

void show() {
  show_one("&derived::foo",
           &derived::foo); // => 「&derived::foo: 9:0」を出力
  show_one("&derived::bar",
           &derived::bar); // => 「&derived::bar: 1:0」を出力
  show_one("&derived::baz",
           &derived::baz); // => 「&derived::baz: 1:8」を出力
  show_one("&derived::qux",
           &derived::qux); // => 「&derived::qux: 1:10」を出力
}

見てのとおり、ary[0] が仮想関数の番号(なにが 9 なのかはよくわからないが...)。ary[1] にクラスの番号が入っているっぽい。
仮想関数表が複数ある場合にこうなるのだろう。と思う。

メンバへのポインタの使い所

型(たとえば関数なら、引数の型と返戻値の型)が揃っている 非 static メンバがいくつかあって、そのどれを使うのかがコンパイルした時点ではわからない(データファイルで決まるとか、ユーザーの操作で決まるとか)場合に使う。

たまに「どのメンバを使うのかを決める enum みたいなのがあって、switchcase で分岐してどのメンバを使うか決める」みたいなコードをみるけど、そういうのは メンバへのポインタの使い所。使っていこう。

C言語 + offsetof と違って型安全なので、避けるべき理由はない。

メンバ関数へのポインタの場合。

ラムダでくるんで std::function に入れれば「非 static メンバへのポインタ」ではない方法で実現できるけれど、そこにはオーバーヘッドがあるし、迂遠な感じもする。this をキャプチャすると 削除済み this へのアクセスの危険性が高まったりもする。

ということで、メンバ関数の型を揃えられる場合、メンバ関数へのポインタ が優れた選択肢になる。

ただ、使いたいんだけど型が揃っていなんだよなぁ、ということがある。そういうときには

  • 無理にでも揃えてメンバ関数へのポインタを使う
  • 諦めて std::function + ラムダ にする

の二択から選ぶ(ほかにもあれば、それでもいいけどなんかある?)。組み込み用途などでは後者はわりと選べないので前者で頑張りがち。

メンバ関数へのポインタは、インスタンス不要関数 3 へのポインタと同程度に型安全なので、同程度に安全。特に避けるべき理由はない。

「メンバのポインタ」ではないポインタとの違い

以後、「メンバのポインタ」ではないポインタのことを「普通のポインタ」と呼ぶ。

offsetof マクロを使ったことがある人には「メンバへのポインタは、C 言語の offsetof とほぼ同じで、構造体の先頭からのオフセットみたいなものだよ。だから普通のポインタとは違うよ」で納得してもらえると思う。

offsetof マクロを使ったことがない人のためにも書いておくと。

普通のポインタは、そのプロセスが使っているメモリのどこにオブジェクトがあるのかがわかる。

*(普通のポインタ) で、オブジェクトへの参照が手に入る。

「メンバへのポインタ」は、オブジェクト内の相対的な場所。住所に例えるなら、部屋番号のみみたいなやつで、建物を特定しないとどの部屋なのかわからない。

(オブジェクト).*(メンバへのポインタ) あるいは (オブジェクトへの普通のポインタ)->*(メンバへのポインタ) で、オブジェクトへメンバであるオブジェクトの参照が手に入る。

あと。

foo->bar()foo というポインタが指すオブジェクトに生えている bar というメンバ関数を呼んでいる 4foo はポインタで、bar はメンバ名。

(foo->*baz)() は「foo というポインタが指すオブジェクトに生えているメンバ関数を呼んでいるんだけど、どのメンバ関数を呼ぶのかは baz という「メンバ関数へのポインタ」の値で決まる。foo はポインタで、baz は「メンバ関数へのポインタ」。

まとめ

C++ には C言語で使われていた「offsetof」マクロの代わりに「メンバへのポインタ」という文法が導入されている。

C++
struct foo {
  int x, y;
  void hoge();
  void fuga();
};

に対して

C++
int foo::*xory = &foo::x;
void (foo::*memfunc)() = &foo::hoge;

と宣言・初期化して、

C++
foo obj;
foo *ptr = &obj;
obj.*xory = 1;
(obj.*memfunc)();
ptr->*xory = 2;
(ptr->*memfunc)();

のように使う。

「メンバ関数へのポインタ」が仮想関数を指している場合にどの関数が呼ばれるのかは、通常の呼び出しと同じになる。

「メンバ関数へのポインタ」のサイズは、sizeof(size_t)*2 のことが多そう。2
それ以外の「メンバへのポインタ」のサイズは sizeof(size_t) になるだろう。

使い所は、クラス(構造体)の中に型が同じメンバがあり、そのどれを使うのかが動的に決まる場合。

型安全なので、安全性などは通常のポインタと同様。必要な場面では積極的に使っていきたい。

  1. 新し目の gcc や clang では__builtin_offsetof というコンパイラに組み込まれた機能になっている

  2. 関数へのポインタのサイズは size_t より大きいかもしれないとか、いろいろある。 2

  3. 「インスタンス不要関数」は、標準的な用語ではない。ここでは staticメンバ関数 またはstatic関数 または グローバル関数 という意味。

  4. foo->bar()foo というポインタが指すオブジェクトのメンバである bar というポインタが指す先にある「インスタンス不要関数」を呼んでいるのかもしれない。この場合も foo はポインタで、bar はメンバ名。

8
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?