TBD

C++: private メンバにアクセス - テンプレート明示的実体化の時に private メンバのポインタが取れる

C++ Advent Calendar を見たら private メンバにアクセスする記事が2日連続している [1,2]。 記事 [1] は自前のライブラリを使っていて説明は別記事で行っている [3]。 記事 [3] では2つの方法について言及していて、friend によるものと明示的テンプレート実体化によるものだそうだ。しかし具体的な説明は更に別の記事を参照している。 記事 [2] は本気なのかネタなのか #define private public である (使い古されたネタなのである)。

明示的実体化による方法が具体的にどういうものなのか気になったので調べてみた。 序でなので別の方法についてもまとめる。

テンプレートの明示的実体化を使う手法

しかし記事 [3] で紹介されているリンク [4] を見ると「非公開に設定されています」となって見られない (追記: Wayback Machineアーカイブが残っていた)。検索すると英語の記事が見つかった [5]。

コードを読んでみると「テンプレートの明示的実体化のテンプレート実引数で private メンバポインタが取得できる」[10] ということを利用しているようだ。知らなかった。

自分でやってみる

元の記事 [5] のコードはもっと単純化できそうなので自分でやってみる。 取り敢えずターゲットは以下のクラスのデータメンバ A::a

class A { int a = 12345; }

こんな感じにしたらアクセスできる (wandbox):

int A::*memptr;
template<int A::*mp> struct Initializer { inline static auto dummy = memptr = mp; };
template struct Initializer<&A::a>;

int main() {
  A obj;
  std::cout << obj.*memptr << std::endl;
}

使いやすいようにまとめてみる

毎回記述するのは面倒なのでまとめる (wandbox):

// 準備
namespace TBD {
  template<typename MemPtr, typename Tag>
  struct accessor {
    inline static MemPtr ptr;
    template<MemPtr mp> struct init { inline static auto dummy = ptr = mp; };
  };
}

// 使う時
using A_a = TBD::accessor<int A::*, struct tag_a>;
template struct A_a::init<&A::a>;
int main() {
  A obj;
  std::cout << obj.*A_a::ptr << std::endl;
}

マクロにしてしまった方が楽かもしれない

余り使いたくないけれども、マクロの方がすっきりするかも (wandbox):

// 準備
#define TBDDeclareMemberPointer(MemPtr, Name, Member) \
  using TBDAccessType_##Name = MemPtr; \
  static TBDAccessType_##Name Name; \
  template<TBDAccessType_##Name value> struct TBDAccessFiller_##Name { inline static auto dummy = Name = value; }; \
  template struct TBDAccessFiller_##Name<&Member>;

// 使う時
TBDDeclareMemberPointer(int A::*, memptr, A::a);
int main() {
  A obj;
  std::cout << obj.*memptr << std::endl;
}

この方法だと依存クラスに対する処理を書けない (明示的実体化を依存文脈に書けない為)。 しかし、テストが目的だとすれば結局全て明示的に書くことになる気がするので余り気にならないのだろう。 いざ必要になれば我慢して使う。

他の手法

序でに、他にどのような方法があったかまとめ。

1 #define private public

これは冗談の域を出ない。色々な悪いことが起こりそうなのは想像に難くない。そもそも、

class A { int a; }

のような定義の private メンバに関しては効果がない。 だからと言って #define class struct も追加すると今度は

template<class T> void f() {}

の様な記述をしていた時にコンパイルできなくなる。

更に記事 [6] に

さらに予約語を#defineするのは非合法でもある。

と書かれている。例によってリンク先 [7] が非公開に設定されているが、検索すると規格の [macro.names]/2 に記述がある様だ。

2 friend を設定する ○

これが一番自然な方法だろう。但し、これは対象本体に手を入れなければならない。

本体を弄れない時にどうするか。 記事 [6] で紹介している Exceptional C++ Style 15章 では、 自分で偽物のヘッダを作って"偽装" しているそうだ。 でもそれだと匿名名前空間で定義されているクラスなどに対して使えない。更に ODR 違反である。

3 同じレイアウトのクラス型に reinterpret_cast する ✕

これも Exceptional C++ Style 15章 にあるそうだ [6]。 然し、これは標準レイアウトクラスでなければ動作を保証できない。一般には未定義の動作である。

4 テンプレート特殊化を利用する △

これも Exceptional C++ Style 15章 から [6]。 対象クラスに関数テンプレートメンバがあれば、その関数テンプレートの特殊化を作成してその中でメンバにアクセスすることができる。 しかし、関数テンプレートメンバがなければ使えない手法である。単純なクラスの場合大抵は関数テンプレートメンバはない。