LoginSignup
5
6

More than 5 years have passed since last update.

メンバへのポインタ(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))

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

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

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

しかし。C++ のメンバには関数もある。
メンバ関数からもメンバへのポインタは取れる。こんな

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」を出力
}

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

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

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] にクラスの番号が入っているっぽい。
仮想関数表が複数ある場合にこうなるのだろう。と思う。

まとめ

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

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 より大きいかもしれないとか、いろいろある。 

5
6
0

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
5
6