2012-01-11
■[C++]privateメンバに外部から非侵入的にアクセスする
はじめに
C++では通常、クラスのprivateメンバに外部からアクセスすることができない。
アクセスするためには、friend関数やfriendクラスを用いる。
しかし、
http://bloglitb.blogspot.com/2010/07/access-to-private-members-thats-easy.html
で話題となり、Daveが、
https://gist.github.com/1528856
にエッセンスを抽出したコードを起こしているが、privateメンバに外部から合法的にアクセスする方法がある。
Daveのコードのコメントだけで必要十分かもしれないが、私は理解するのにかなり時間がかかってしまったので、その経緯を忘れないためにもここに書いておきたいと思う。
メンバポインタの復習
まずは、このコードを見て欲しい。
Accessorというクラステンプレートを用いて、クラスAのpublicなメンバ、mem1,mem2にアクセスしている。
クラスAのint型メンバへのメンバポインタ型は、
int A::*
であり、値は、
&A::mem1
などと表現される。
ここで、&A::mem1 はあくまでもクラスAのレイアウトにおける先頭からのオフセットを示すに過ぎない。
.*演算子の左辺値としてオブジェクトを与えることで、アドレスが定まり、参照外しが行われる。
よって、以下のように書くことで、a.mem2にアクセスすることができる。
a.*&A::mem2
先ほど、&A::mem1は、オフセットであることを述べたが、この値は、コンパイル時定数となる。よって、このようにテンプレートパラメタとして渡すことが可能である。
template <class MemPtr, MemPtr p> struct Accessor { static MemPtr value() { return p; } };
このクラステンプレートAccessorを利用して、
a.*&A::mem2
と同じことを実現しているのが、
a.*Accessor<int A::*, &A::mem2>::value()
である。
もちろん、このような &A::mem2 への外部からのアクセスは、クラスAにおけるメンバmem2がpublicであるから可能なのであり、もしこれがprivateであったならば、アクセス制限に違反するととになり結果としてコンパイルエラーとなる。
privateメンバにアクセスできる場所
クラス定義の外部で、そして、当該クラスのメンバ関数定義の外部で、合法的にクラスのprivateメンバにアクセスできる場所が一つだけある。それが、テンプレートの明示的な実体化、explicit instantiationを行う箇所である。
N3290には、explicit instantiation に関して以下の記述がある。
14.7.2/12 The usual access checking rules do not apply to names used to specify explicit instantiations.
[ Note: In particular, the template arguments and names used in the function declarator (including parameter types,return types and exception specifications) may be private types or objects which would normally not be accessible and the template may be a member template or member function which would not normally be accessible. ―end note ]
おそらく、privateなメンバを explicit instantiation させたいことがあるから、access checkingが行われないのであろう。
なお、これはC++03においても同様である。
本アプローチの戦略
さて、材料はそろった。本アプローチは、privateメンバへのクラス外部からの合法的なアクセスが可能な、テンプレートのexplicit instantiationにおいて、アクセスしたいprivateメンバのメンバポインタを取得し、それを何らかの形で保持し、後からアクセスしたいときには保持したメンバポインタを経由してprivateメンバにアクセスしてやろうというものだ。
これを実現するコードを以下に示す。
まず、int main()を見てみよう。先ほどのpublicなメンバへのアクセスに似ていると思う。一番の違いは、&A::mem2が消えていることだ。ここで、&A::mem2を使うと、アクセス制限に引っかかりコンパイルエラーとなる。
int main() { A a(1, 2); // Accessorを経由して (設定済みの )mem にアクセス std::cout << a.*Accessor<A_mem1>::value << std::endl; std::cout << a.*Accessor<A_mem2>::value << std::endl; a.*Accessor<A_mem1>::value = 3; a.*Accessor<A_mem2>::value = 4; a.print(); // ちなみに、Explicit instantiation 以外の箇所では // private メンバにアクセスできない // Initializer<A, &A::mem>(); // error A::mem is private }
では、順を追って動きを見ていこう。
まずは、コンパイル時の動きから。
1.explicit instantiation
以下のコードで、クラステンプレートInitializer(後述)のexplicit instantiationを行っている。ここが唯一 A::mem2 にクラス外からアクセスできる場所である。
template struct Initializer<A_mem2, &A::mem2>;
2.実体化するInitializer
// ---------------------------------------------------------- // Initializer template <class Tag, typename Tag::type p> struct Initializer { // コンストラクタにおいて // Accessor型のstaticメンバ value に p (メンバポインタ) // を設定する。 Initializer() { Accessor<Tag>::value = p; } static Initializer instance; }; // 初期化を駆動するための static オブジェクト(自身)の定義 // explicit instantiation によって、int main() 前に // 生成されることが確定し、生成の際、コンストラクタが // 呼び出されることになる。 template <class Tag, typename Tag::type p> Initializer<Tag, p> Initializer<Tag, p>::instance;
を2つのテンプレートパラメタA_mem2と&A::mem2を渡して実体化させている。
A_mem2の定義は以下のようになっている。アクセスしたいメンバすなわちA::mem2と同じ型(つまりint)の、そして当該メンバが所属するクラス(つまりA)のメンバポインタ型 type を typedefしている。
struct A_mem2 { typedef int A::* type; };
よって、TagにはA_mem2が、p は &A::mem2でそれぞれ設定され、Tag::typeは、int A::* となる。
3.staticメンバinstanceの実体化
クラステンプレートInitializerの実体化に伴い、そのstaticメンバinstanceも実体化する。
実体化したイメージを表現すると以下のようになる。
Initializer<A_mem2, &A::mem2> Initializer<A_mem2, &A::mem2>::instance;
そして、これに伴い、Initializerのコンストラクタが実体化する。
Initializerのコンストラクタは以下のように定義されており、
Initializer() { Accessor<Tag>::value = p; }
実体化のイメージを表現すると以下のようになる。
Initializer() { Accessor<A_mem2>::value = &A::mem2; }
4.Accessor(クラステンプレートおよびstaticメンバvalue)の実体化
先ほどのコンストラクタの定義内にて、Accessor<A_mem2>::value が実体化する。
Accessorの定義は以下の通りであり、
template <class Tag> struct Accessor { static typename Tag::type value; };
実体化のイメージを表現すると以下のようになる。
template <class A_mem2> struct Accessor { static int A::* value; };
ここまでがコンパイル時の話だ。
そしてここからは実行時の話。
int main()が開始するまでに、大域オブジェクトの初期化が行われる。
5.static int A::* value の初期化
初期値が与えられていないので、0で初期化される
6.Initializer<A_mem2, &A::mem2> Initializer<A_mem2, &A::mem2>::instanceの初期化
初期値はないがデフォルトコンストラクタが定義されているので、そのデフォルトコンストラクタが呼び出される。
先ほどの実体化イメージに従い、以下のコードが実行される。
Initializer() { Accessor<A_mem2>::value = &A::mem2; }
これはつまり、先ほど0で初期化された value に &A::mem2 が代入されるということだ。
大域オブジェクトに関する処理は以上となる。
7.int main()の実行
privateメンバにアクセスする部分を見ると、以下のようなコードとなっている。
std::cout << a.*Accessor<A_mem2>::value << std::endl;
Accessor<A_mem2>::value には、&A::mem2が代入されている。
単純に置換してみると、冒頭で述べた、
a.*&A::mem2
と全く同じであることが分かる。
この結果、privateメンバにアクセスすることができるというわけだ。
アクセス指定が回避されたポイント
さて、一連の7ステップの流れの中で、実体化のイメージと称して、&A::mem2という記述を何度か行った。
しかし、Step1の
template struct Initializer<A_mem2, &A::mem2>;
(explicit instantiationのため、&A::mem2へのアクセスは合法)
から、Step2のテンプレートパラメタ p に&A::mem2が渡った段階で、既に、p は単なる int A::* 型の値となっており、以降、アクセス指定子がチェックされることは無いというわけだ。
template <class Tag, typename Tag::type p> struct Initializer { // コンストラクタにおいて // Accessor型のstaticメンバ value に p (メンバポインタ) // を設定する。 Initializer() { Accessor<Tag>::value = p; } static Initializer instance; };
最後に、サンプルのコード全体を貼り付けておく(ideoneのものと同じ)
// privateメンバへのアクセス サンプル // 元となるコード // https://gist.github.com/1528856 // Accessing Private Data // c.f. http://bloglitb.blogspot.com/2010/07/access-to-private-members-thats-easy.html #include <iostream> // ---------------------------------------------------------- // Accessor // Tag には、後述のA_mem1やA_mem2が渡される。 // Tag::type は、アクセスするメンバのメンバポインタ型となる。 template <class Tag> struct Accessor { static typename Tag::type value; }; // staticメンバの実体 // アクセスしたい型のメンバポインタを保持する。 template <class Tag> typename Tag::type Accessor<Tag>::value; // ---------------------------------------------------------- // Initializer template <class Tag, typename Tag::type p> struct Initializer { // コンストラクタにおいて // Accessor型のstaticメンバ value に p (メンバポインタ) // を設定する。 Initializer() { Accessor<Tag>::value = p; } static Initializer instance; }; // 初期化を駆動するための static オブジェクト(自身)の定義 // explicit instantiation によって、int main() 前に // 生成されることが確定し、生成の際、コンストラクタが // 呼び出されることになる。 template <class Tag, typename Tag::type p> Initializer<Tag, p> Initializer<Tag, p>::instance; // ------------------------------------------------------ // Target Class // privateメンバにアクセスされるクラス struct A { A(int mem1, int mem2):mem1(mem1), mem2(mem2) {} void print() const { std::cout << "mem1 = " << mem1 << " mem2 = " << mem2 << std::endl; } private: int mem1; int mem2; }; // Tagクラス。アクセスするメンバの、メンバポインタ型を // をタイプメンバ type として持つ struct A_mem1 { typedef int A::* type; }; struct A_mem2 { typedef int A::* type; }; // ------------------------------------------------------ // Template の Explicit instantiation // ここでは private メンバにアクセスできる // A::memのアドレスで、Initialize経由でAccessor::valueを // 設定 // 14.7.2/12 The usual access checking rules do not apply // to names used to specify explicit instantiations. // [ Note: In particular, the template arguments and // names used in the function declarator (including // parameter types,return types and exception // specifications) may be private types or objects // which would normally not be accessible and the // template may be a member template or member function // which would not normally be accessible. ―end note ] template struct Initializer<A_mem1, &A::mem1>; template struct Initializer<A_mem2, &A::mem2>; int main() { A a(1, 2); // Accessorを経由して (設定済みの )mem にアクセス std::cout << a.*Accessor<A_mem1>::value << std::endl; std::cout << a.*Accessor<A_mem2>::value << std::endl; a.*Accessor<A_mem1>::value = 3; a.*Accessor<A_mem2>::value = 4; a.print(); // ちなみに、Explicit instantiation 以外の箇所では // private メンバにアクセスできない // Initializer<A, &A::mem>(); // error A::mem is private } // Output: /* 1 2 mem1 = 3 mem2 = 4 */
2012/01/14追記
その後、privateな、クラス内で定義された型を持つメンバやstaticメンバについて、この手法が適用可能かTwitter上で以下のやりとりがあった。
2012-01-12 11:13:27 via Echofon to @TKinugasa
@redboltz: @TKinugasa いやまてよ。privateなstaticはいける気がします。typeは無理そうですが。
2012-01-12 11:20:39 via Echofon to @TKinugasa
今の時点の結論としては、
- staticなprivateメンバへのこの手法の適用は可能。 http://ideone.com/qzfC0
- privateなクラス内定義型のメンバ(staticか否かに関係なく)には、この手法は適用できない。
というもの。
クラス内定義型が、クラス外からアクセス可能な型のtypedefなら、まあ、その元の型を指定することで、アクセスは可能だが、それを持って適用可能だとは言えないだろうし、ネストクラスの場合はどうしようもない。
- 79 http://t.co/WIruP9d3
- 67 http://t.co/JLISbt8W
- 39 http://www.google.co.jp/url?sa=t&rct=j&q=wimax 1day&source=web&cd=10&ved=0CJMBEBYwCQ&url=http://d.hatena.ne.jp/redboltz/20101112/1289521705&ei=x4oOT6SIH4LMmAWXgrH4Aw&usg=AFQjCNHmn3c069WCBVSdw-d3BShXFOYH0w&sig2=lvlIfpu5EljkzK9lHOenKA
- 29 http://t.co/eFVmOhWW
- 25 http://longurl.org
- 24 http://t.co/pUdDwLKn
- 24 http://www.google.co.jp/url?sa=t&rct=j&q=メモリ 8g 4g 認識&source=web&cd=1&ved=0CCEQFjAA&url=http://d.hatena.ne.jp/redboltz/20100131/1264897391&ei=8JMJT4raKePDmQXard2IB
- 23 http://d.hatena.ne.jp/faith_and_brave/20100601/1275386397
- 21 http://t.co/sz9c52jY
- 20 http://t.co/B9FbTL01
