はじめに
C++には、構造体やクラスのメンバー関数について、その構造体、クラス内での相対的な位置を表すメンバー関数ポインターというものが存在します。メンバー変数についても同じです。
こんなやつ。
#include <iostream>
struct TestDesu {
int value = 1234;
float func(int v, bool b) { return b ? v : 0; }
};
int main(){
// ポインター変数の定義
int TestDesu::* member_variable_pointer; // TestDesuクラスのint型メンバー変数のポインター型
float(TestDesu::* member_function_pointer)(int, bool); // TestDesuクラスのfloat(int, bool)なメンバー関数のポインター型
// ポインターの取得
member_variable_pointer = &TestDesu::value; // TestDesu::valueのメンバー変数ポインター取得
member_function_pointer = &TestDesu::func; // TestDesu::funcのメンバー関数ポインター取得
// ポインターを介した参照
TestDesu test_desu;
std::cout << test_desu.*member_variable_pointer << std::endl; // TestDesu::value参照
std::cout << (test_desu.*member_function_pointer)(4946, true) << std::endl; // TestDesu::func関数呼び出し
}
このメンバー関数ポインターやメンバー変数ポインターの、クラスや値の型を取得したくなることはありませんか?
ありますよね。
あります。
私はありましたが、STLやBoost Libraryにそのようなものが見当たらなさそうだったので、自分で作成しました。
本記事では、その方法について説明します。
本記事のコードは、C++17に対応したコンパイラおよびオプションの使用を前提としています。
記載しているコードの動作確認は以下のコンパイラで行いました。
- clang 10.0.0
- g++ 10.1.0
- MSVC 14.28 (VS 16.8.2)
ソースコード
ソースコードは以下の通りです。
#include <type_traits>
#include <utility>
#include <tuple>
// メンバー変数ポインター関連実装用テンプレート
template <typename C, typename V>
auto member_variable_pointer_t_impl(V C::* p) -> std::pair<C, V>;
// メンバー変数ポインターのクラス型を取得するエイリアステンプレート
template <auto P>
using member_variable_pointer_class_t = typename decltype(member_variable_pointer_t_impl(P))::first_type;
// メンバー変数ポインターの値型を取得するエイリアステンプレート
template <auto P>
using member_variable_pointer_variable_t = typename decltype(member_variable_pointer_t_impl(P))::second_type;
// メンバー関数ポインター関連実装用テンプレート
template <typename C, typename R, typename... Ps>
auto member_function_pointer_t_impl(R(C::* p)(Ps...)) -> std::tuple<C, R, std::tuple<Ps...>>;
// メンバー関数ポインターのクラス型を取得するエイリアステンプレート
template <auto P>
using member_function_pointer_class_t = std::tuple_element_t<0, decltype(member_function_pointer_t_impl(P))>;
// メンバー関数ポインターの戻り値型を取得するエイリアステンプレート
template <auto P>
using member_function_pointer_return_t = std::tuple_element_t<1, decltype(member_function_pointer_t_impl(P))>;
// メンバー関数ポインターの引数型をstd::tupleですべて取得するエイリアステンプレート
template <auto P>
using member_function_pointer_parameters_t = std::tuple_element_t<2, decltype(member_function_pointer_t_impl(P))>;
// メンバー関数ポインターのインデックスで指定した引数型を取得するエイリアステンプレート
template <auto P, size_t I>
using member_function_pointer_parameter_t = std::tuple_element_t<I, member_function_pointer_parameters_t<P>>;
解説
メンバー変数ポインター及びメンバー関数ポインターのクラス型等を取得する仕組みについて、簡単に説明します。
メンバー変数ポインターからの型取り出し
メンバー変数ポインターのクラス型および値型の取得には、以下の二つが関わっています。
- メンバー変数ポインターからそのクラス型と値型を取り出す関数テンプレート(
member_variable_pointer_t_impl
) - メンバー変数ポインターを上記関数テンプレートに渡し、必要な型を取り出すエイリアステンプレート(
member_variable_pointer_class_t
およびmember_variable_pointer_value_t
)
まずは「メンバー変数ポインターからそのクラス型と値型を取り出す関数テンプレート」について説明します。実体は以下に再掲するmember_variable_pointer_t_impl
です。
template <typename C, typename V>
auto member_variable_pointer_t_impl(V C::* p) -> std::pair<C, V>;
この関数テンプレートは、メンバー変数ポインターV C::* p
を仮引数とし、引数に渡されたメンバー変数ポインターのクラス型と値型のstd::pair
を戻り値std::pair<C, V>
として、宣言されています。
テンプレート仮引数は二つ宣言されており、C
がクラス型、V
が値型を表します。この二つのテンプレート仮引数に渡されるテンプレート引数を推論するのが、この関数テンプレートの目的です。
このために、関数テンプレートのテンプレート引数推論を利用します。関数テンプレートのテンプレート引数推論は、仮引数にテンプレート仮引数が使用された関数テンプレートにおいて、テンプレート引数が省略された場合に、関数に渡された引数の型から、仮引数の宣言で使用されているテンプレート仮引数に当てはまるテンプレート引数を推論する機能です。
ここでは、引数p
にメンバー変数ポインターを渡すことで、そのクラス型C
と値型V
が推論されます。
なお、この関数テンプレートは上記の目的から、「その戻り値の型のみ」が分かれば十分です。よって、この関数を呼び出すことはないため、関数の実装は定義しません。
次に、「メンバー変数ポインターを上記関数テンプレートに渡し、必要な型を取り出すエイリアステンプレート」です。こちらの実体は、member_variable_pointer_class_t
とmember_variable_pointer_variable_t
です。この二つが行っていることは同じなので、member_variable_pointer_class_t
で説明します。
template <auto P>
using member_variable_pointer_class_t = typename decltype(member_variable_pointer_t_impl(P))::first_type;
このエイリアステンプレートは、テンプレート引数P
としてメンバー変数ポインターを受け取り、それを上記のmember_variable_pointer_t_impl
に渡し、その戻り値型から取得できるメンバー変数ポインターのクラス型のエイリアスを定義します。
行っていること自体はmember_variable_pointer_t_impl
への橋渡しだけです。型エイリアスとして使用者が使用しやすい形で提供する目的で用意しています。
このエイリアステンプレートでは、メンバー変数ポインターをテンプレート引数として受け取るために、C++17から導入された「非型テンプレートパラメータのauto宣言」を使用しています。C++14以前では、テンプレート引数としてメンバー変数ポインター等を渡す際には、メンバー変数ポインタの具体的な型を定義する必要があり、そのためにクラス型と値型を事前に知る必要がありました。クラス型を知らないから知りたいのに、そのために事前にクラス型を知らないといけないというのは本末転倒です。それが、C++17のこの機能を使うことにより、容易に回避可能になりました。
さて、テンプレート引数として受け取ったメンバー変数ポインターをmember_variable_pointer_t_impl
に渡すことで、クラス型と値型を推論することができるわけですが、その結果を取り出すには、member_variable_pointer_t_impl
の戻り値の型をどうにかして知る必要があります。
これはdecltype
により可能です。decltype
を関数呼び出し式に対して使用すると、その関数の戻り値の型を得ることができます。
member_variable_pointer_t_impl
の戻り値の型はstd::pair<クラス型, 値型>
であり、そのうちの一つ目が欲しいため、ここではfirst_type
により一つ目の型を取り出し、完了です。
メンバー関数ポインターからの型取り出し
基本的な部分はメンバー変数ポインターの場合と同じです。
異なる点は、メンバー関数の引数の数は可変であり、メンバー関数ポインターからの型取得ではその可変な引数の数に対応する必要があるという点です。
メンバー関数ポインターからの各種型の取り出しを行うmember_function_pointer_t_impl
の定義を以下に再掲します。
template <typename C, typename R, typename... Ps>
auto member_function_pointer_t_impl(R(C::* p)(Ps...)) -> std::tuple<C, R, std::tuple<Ps...>>;
メンバー変数ポインターの場合との大きな違いは、テンプレート仮引数の宣言において、可変長テンプレート引数を使用している点です。
C
がクラス型、R
が戻り値型で、Ps
が可変長な引数型です。
これは上記の問題に対応するためのもので、関数の仮引数を可変長テンプレート引数の展開を使用して宣言し(R(C::* p)(Ps...)
の部分)、そこにメンバー関数ポインターを受け取ります。これにより、すべての引数の型を推論することができます。
推論された複数の関数の引数の型は、std::tuple
により返します。
これにより、member_function_pointer_parameters_t
で関数の全引数の型をstd::tuple
で取得できます。しかし、特定の位置の引数の型のみを取り出したい場合もあると思います。
その場合は、std::tuple_element_t
テンプレートエイリアスにより取り出し可能ですが、そのコードを使用者が毎回記述するのは使用者にとって面倒くさいでしょう。
ということで、std::tuple_element_t
による型の取り出しまで行ってくれるmember_function_pointer_parameter_t
も用意しました。
使用例
基本的に、第一テンプレート引数にメンバー変数ポインタやメンバー関数ポインターを渡すだけです。
member_function_pointer_parameter_t
のみ、型を取得したい引数のインデックスも渡してください。
以下が使用例のコードです。取得した型の型名をtypeid
により取得し、標準出力に出力しています。
"member_pointer_traits.hpp"は、本記事で説明した各種テンプレートの定義が含まれるヘッダーファイル(内容は上記のコードそのまま)です。
#include <iostream>
#include "member_pointer_traits.hpp"
struct TestDayo {
int value;
float func(int v, bool f){return 0;}
};
int main(){
std::cout << typeid(member_variable_pointer_class_t<&TestDayo::value>).name() << std::endl;
std::cout << typeid(member_variable_pointer_variable_t<&TestDayo::value>).name() << std::endl;
std::cout << typeid(member_function_pointer_class_t<&TestDayo::func>).name() << std::endl;
std::cout << typeid(member_function_pointer_return_t<&TestDayo::func>).name() << std::endl;
std::cout << typeid(member_function_pointer_parameter_t<&TestDayo::func, 0>).name() << std::endl;
std::cout << typeid(member_function_pointer_parameter_t<&TestDayo::func, 1>).name() << std::endl;
}
g++での出力は以下の通りです。
8TestDayo
i
8TestDayo
f
i
b
typeid
演算子が返すtype_info
型のメンバー関数name()
は、実装依存の型名を返します。
g++の場合、i
はint型、f
はfloat型、b
はbool型を表しています。
このことから、意図した結果となっていることが分かります。