ポインターが難しいと言われる理由の1つに、ポインターの文法が難しい問題がある。
型としてのポインターは、ある型T
があるときに、T
へのポインター型となる。
T
へのポインター型はT *
と書く。
// intへのポインター型
using t1 = int * ;
// doubleへのポインター型
using t2 = double * ;
// std::stringへのポインター型
using t3 = std::string * ;
// std::array<int,5>へのポインター型
using t4 = std::array<int,5> * ;
// std::array<double,10>へのポインター型
using t5 = std::array<double,10> * ;
リファレンスやconst
も同じだ。
// int型へのポインター型
using t1 = int * ;
// int型へのリファレンス型
using t2 = int & ;
// どちらも同じconstなint型
using t3 = const int ;
using t4 = int const ;
const int
とint const
は同じ型だ。この場合、const
はint
型のあとに付いても前に付いても同じ意味になる。
すると当然の疑問が生じる。組み合わせるとどうなるのかということだ。
ポインター型へのリファレンス型はできる。
// int *型へのリファレンス
using type = int * & ;
リファレンス型へのポインター型はできない。
// エラー、できない
using error = int & * ;
理由は、リファレンスへのポインターというのは意味がないからだ。ポインターへのリファレンスは意味がある。
リファレンスからポインターの値を得るには、参照先のオブジェクトと同じく&
を使う。
int data { } ;
int & ref = data ;
// &dataと同じ
int * ptr = &ref ;
リファレンスは参照先のオブジェクトとまったく同じように振る舞うのでリファレンス自体のポインターの値を得ることはできない。
ポインターのリファレンスを得るのは、ポインター以外の値とまったく同じだ。
int * ptr = nullptr ;
// ptrを参照する
int * & ref = ptr ;
int data { } ;
// ptrの値が&dataになる。
ref = &data ;
const
とポインターの組み合わせは難しい。
まず型T
とそのconst
版がある。
using T = int ;
using const_T = const T ;
そして型T
とそのポインター版がある。
using T = int ;
using T_pointer = T * ;
これを組み合わせると、以下のようになる。
// 型T
using T = int ;
// どちらもconstなT
using const_T_1 = const T ;
using const_T_2 = T const ;
// Tへのポインター
using T_pointer = T * ;
// どちらもconstなTへのポインター
using const_T_pointer_1 = const T * ;
using const_T_pointer_2 = T const * ;
// Tへのconstなポインター
using T_const_pointer = T * const ;
// どちらもconstなTへのconstなポインター
using const_T_const_pointer_1 = const T * const ;
using const_T_const_pointer_2 = T const * const ;
順番に見ていこう。まずは組み合わせない型から。
using T = int ;
// どちらもconstなT
using const_T_1 = const T ;
using const_T_2 = T const ;
// Tへのポインター
using T_pointer = T * ;
T
はここではint
型だ。T
型はどんな型でもよい。
const T
とT const
が同じ型であることを思い出せば、const_T_1
とconst_T_2
は同じ型であることがわかるだろう。
T_pointer
はT
へのポインターだ。
次を見ていこう。
// どちらもconstなTへのポインター
using const_T_pointer_1 = const T * ;
using const_T_pointer_2 = T const * ;
これはどちらも同じ型だ。const
なT
へのポインターとなる。わかりにくければ以下のように書いてもよい。
// constなT
using const_T = const int ;
// constなTへのポインター
using const_T_pointer = const_T * ;
実際に使ってみよう。
int main()
{
const int data = 123 ;
// int const *でもよい
const int * ptr = &data ;
// 読み込み
int read = *ptr ;
}
const
なint
へのポインターなので、このポインターの参照先を変更することはできない。ポインターは変更できる。
int main()
{
const int x {} ;
const int * ptr = &x ;
// エラー
// constな参照先を変更できない
*ptr = 0 ;
int y {} ;
// OK
// ポインターはconstではないので値が変更できる
ptr = &y ;
}
const
なのはint
であってポインターではない。const int *
、もしくはint const *
は参照先のint
がconst
なので、参照先を変更することができない。ポインターはconst
ではないので、ポインターの値は変更できる。
const
なT
型へのリファレンスでconst
ではないT
型のオブジェクトを参照できるように、const
なT
型へのポインターからconst
ではないT
型のオブジェクトを参照できる。
int main()
{
// constではない
int data { } ;
// OK
const int & ref = data ;
// OK
const int * ptr = &data ;
}
この場合、リファレンスやポインターはconst int
扱いなので、リファレンスやポインターを経由して読むことはできるが変更はできない。
int main()
{
int data = 123 ;
const int * ptr = &data ;
// エラー
// 変更できない
*ptr = 0 ;
// 変更できる
data = 0 ;
}
その次はconst
なポインターだ。
// Tへのconstなポインター
using T_const_pointer = T * const ;
これはポインターがconst
なのであって、T
はconst
ではない。したがってポインターを経由して参照先を変更することはできるが、ポインターの値自体は変更できない型だ。
int main()
{
int data { } ;
// constなポインター
int * const ptr = &data ;
// OK、参照先は変更できる
*ptr = 1 ;
// エラー、値は変更できない
ptr = nullptr ;
}
最後はconst
なT
へのconst
なポインターだ。
// どちらもconstなTへのconstなポインター
using const_T_const_pointer_1 = const T * const ;
using const_T_const_pointer_2 = T const * const ;
これはconst
なT
なので、ポインターを経由して参照先を変更できないし、const
なポインターなのでポインターの値も変更できない。
int main()
{
int data = 123 ;
int const * const ptr = &data ;
// OK、参照先は読める
int read = *ptr ;
// エラー、参照先は変更できない
*ptr = 0 ;
// エラー、ポインターは変更できない
ptr = nullptr ;
}
ポインター型というのは、「ある型T
へのポインター」という形で表現できる。この型T
にはどんな型でも使うことができる。ところで、ポインターというのは型だ。もしT
がポインター型の場合はどうなるのだろう。
例えば、「T
型へのポインター型」で、型T
が「U
型へのポインター型」の場合、全体としては「U
型へのポインター型へのポインター型」になる。これはC++の文法ではU **
となる。
C++のコードで確認しよう。
// 適当なU型
using U = int ;
// ポインターとしてのT型
using T = U * ;
// T型へのポインター型
// つまりU型へのポインター型へのポインター型
// つまりU **
using type = T * ;
具体的に書いてみよう。
int main()
{
// int
int x = 123 ;
// intへのポインター
int * p = &x ;
// intへのポインターのポインター
int ** pp = &p ;
// 123
// ポインターを経由したポインターを経由したxの読み込み
int value1 = **pp ;
int y = 456 ;
// ポインターを経由した変数pの変更
*pp = &y ;
// 456
// ポインターを経由したポインターを経由したyの読み込み
int value2 = **pp ;
}
x
はint
だ。p
はint
へのポインターだ。ここまではいままでどおりだ。
pp
はint **
という型で、「int
へのポインターへのポインター」型だ。このポインターの値のためには「int
へのポインターのポインター」が必要だ。変数p
のポインターは&p
で得られる。この場合、変数p
は「int
へのポインター」でなければならない。そうした場合、変数p
のポインターは「int
へのポインターのポインター」型の値になる。
変数pp
は「int
へのポインターのポインター」だ。変数pp
の参照先の変数p
を読み書きするには、*pp
と書く。これはまだ「int
へのポインター」だ。ここからさらに参照先のint
型のオブジェクトにアクセスするには、その結果にさらに*
を書く。結果として**pp
となる。
わかりにくければ変数に代入するとよい。
int main()
{
int object { } ;
int * a = &object ;
int ** b = &a ;
// cとaは同じ値
int * c = *pointer_to_pointer_to_object ;
// objectに1が代入される
*c = 1 ;
// objectに2が代入される
**b = 2 ;
}
リファレンスを使うという手もある。
int main()
{
int object { } ;
int * a = &object ;
int ** b = &a ;
int & r1 = *a ;
// objectに1が代入される
r1 = 1 ;
int &r2 = **b ;
// objectに2が代入される
r2 = 2 ;
}
「ポインターへのポインター」があるということは、「ポインターへのポインターへのポインター」もあるということだろうか。もちろんある。
// intへのポインターへのポインターへのポインター型
using type = int *** ;
// intへのポインターへのポインターへのポインターへのポインター型
// int ****
using pointer_to_type = type * ;
もちろんconst
も付けられる。
using type = int const * const * const * const ;
関数へのポインターを説明する前に、まず型としての関数を説明しなければならない。
関数にも型がある。例えば以下のような関数、
int f( int ) ;
double g( double, double ) ;
の型は、
using f_type = int ( int ) ;
using g_type = double ( double, double ) ;
となる。関数から関数名を取り除いたものが関数の型だ。すると関数へのポインター型は以下のようになる。
using f_pointer = f_type * ;
using g_pointer = g_type * ;
さっそく試してみよう。
// 実引数を出力して返す関数
int f( int x )
{
std::cout << x ;
return x ;
}
int main()
{
using f_type = int ( int ) ;
using f_pointer = f_type * ;
f_pointer ptr = &f ;
// 関数へのポインターを経由した関数呼び出し
(*ptr)(123) ;
}
動くようだ。最後の関数呼び出しはまず参照先を得て(*ptr)
、その後に関数呼び出し(123)
をしている。これは面倒なので、C++では特別に関数へのポインターはそのまま関数呼び出しすることができるようになっている。
// 関数へのポインターを経由した関数呼び出し
ptr(123) ;
ところで、変数ptr
の宣言を、f_pointer
というエイリアス宣言を使わずに書くと、以下のようになる。
// 適当な関数
int f( int ) { return 0 ; }
// 変数ptrの宣言
// int (int)へのポインター
int (*ptr)(int) = &f ;
なぜこうなるのか。これを完全に理解するためにはC++の宣言子(declarator)という文法の詳細な理解が必要だ。
ここでは詳細を飛ばして重要な部分だけ伝えるが、型名のうちポインターであることを指定する*
は、名前にかかる。
// この *はnameにかかる
int * name ;
つまり以下のような意味だ。
int (*name) ;
型名だけを指定する場合、名前が省略される。
// 名前が省略されている
using type = int * ;
つまり以下のような意味だ。
using type = int (*) ;
そのため、int * name( int )
と書いた場合、これは「int
型の引数を取り、int
型へのポインターを戻り値として返す関数」となる。
int * f( int ){ return nullptr ; }
そうではなく、「int
型の引数を取りint
型の戻り値を返す関数へのポインター」を書きたい場合は、
using type = int (*)(int) ;
としなければならない。
変数の名前を入れる場所は以下のとおり。
using type =
int
( * // ポインター
// ここに変数が省略されている
)(int) ;
なので、
int (*ptr)(int) = nullptr ;
となる。あるいは以下のように書いてもいい。
using function_type = int (int) ;
using function_pointer_type = function_type * ;
function_pointer_type ptr = nullptr ;
関数へのポインターは型であり、値でもある。値であるということは、関数は引数として関数へのポインターを受け取ったり、関数へのポインターを返したりできるということだ。
さっそく書いてみよう。
int f( int x ) { return x ; }
using f_ptr = int (*) (int ) ;
// 関数へのポインターを引数に取り
// 関数へのポインターを戻り値として返す
// 関数g
f_ptr g( f_ptr p )
{
p(0) ;
return p ;
}
int main()
{
g(&f) ;
}
これは動く。ところでこの関数g
へのポインターはどう書けばいいのだろうか。つまり、
auto ptr = &g ;
をauto
を使わずに書くとどうなるのだろうか。
以下のようになる。
int (*(*ptr)(int (*)(int)))(int) = &g ;
なぜこうなるのか。分解すると以下のようになる。
int (* // 戻り値型前半
(*ptr) // 変数名
(// 関数の引数
int (*)(int) // 引数としての関数へのポインター
)// 関数の引数
)(int) // 戻り値の型後半
= &g ; // 初期化子
これはわかりにくい。戻り値の型を後ろに書く文法を使うと少し読みやすくなる。
auto (*ptr)( int (*)(int) ) -> int (*)(int) = &g ;
これを分解すると以下のようになる。
auto // プレイスホルダー
(*ptr) // 変数名
( int (*)(int) ) // 引数
-> int (*)(int) // 戻り値の型
= &g ; // 初期化子
もちろん、これでもまだわかりにくいので、エイリアス宣言を使った方がよい。
using func_ptr = int(*)(int) ;
auto (*ptr)(func_ptr) -> func_ptr = &g ;
配列へのポインターについて学ぶ前に、配列の型について学ぶ必要がある。
配列の型は、要素の型をT
、要素数をN
とすると、T [N]
となる。
// 要素型int、要素数5の配列型
using int5 = int [5] ;
// 要素型double、要素数10の配列型
using double10 = double [10] ;
関数型と同じく、ポインター宣言子である*
は名前に付く。
// 要素型int、要素数5の配列へのポインター型
using pointer_to_array_type = int (*)[5] ;
int main()
{
int a[5] ;
pointer_to_array_type ptr = &a ;
}
エイリアス宣言を使わない変数の宣言は以下のようになる。
int main()
{
int a[5] ;
int (*p)[5] = &a ;
}
配列とポインターは密接に関係している。そのため、配列名は配列の先頭要素へのポインターに暗黙に変換される。
int main()
{
int a[5] = {1,2,3,4,5} ;
// &a[0]と同じ
int * ptr = a ;
}
配列とポインターの関係については、ポインターの詳細で詳しく説明する。
T
型へのポインター型はT *
で作ることができる。
ただし、T
がint (int)
のような関数型である場合は、int (*)(int)
になる。配列型の場合は要素数N
まで必要でT (*)[N]
になる。
エイリアス宣言で型に別名を付けるとT *
でよくなる。
using function_type = int (int) ;
using pointer_to_function_type = function_type * ;
ポインターの型を書く際に、このようなことをいちいち考えるのは面倒だ。ここで必要のなのは、ある型T
を受け取ったときに型T *
を得るような方法だ。ところで、物覚えのいい読者は前にも似たような文章を読んだことに気が付くだろう。そう、テンプレートだ。
テンプレートは型を引数化できる機能だ。いままではクラスや関数にしか使っていなかったが、実はエイリアス宣言にも使えるのだ。
template < typename T >
using type = T ;
これは引数と同じ型になるエイリアステンプレートだ。使ってみよう。
template < typename T > using type = T ;
// aはint
type<int> a = 123 ;
// bはdouble
type<double> b = 1.23 ;
// cはstd::vector<int>
type<std::vector<int>> c = {1,2,3,4,5} ;
using type = int ;
というエイリアス宣言があるときtype
の型はint
だ。エイリアス宣言は新しいtype
という型を作るわけではない。
同様に、上のエイリアステンプレートtype
によるtype<int>
の型はint
だ。新しいtype<int>
という型ができるわけではない。
もう少し複雑な使い方もしてみよう。
// int
type<type<int>> a = 0 ;
// int
type<type<type<int>>> b = 0 ;
type<int>
の型はint
なので、それを引数に渡したtype< type<int> >
もint
だ。type<T>
をいくつネストしようともint
になる。
// std::vector<int>
std::vector< type<int> > a = {1,2,3,4,5} ;
// std::vector<int>
type<std::vector<type<int>>> b = {1,2,3,4,5} ;
type<int>
はint
なので、std::vector<type<int>>
はstd::vector<int>
になる。それをさらにtype<T>
で囲んでも同じ型だ。
type<T>
は面白いが何の役に立つのだろうか。type<T>
は型として使える。つまりtype<T> *
はポインターとして機能するのだ。
template < typename T > using type = T ;
// int *
type<int> * a = nullptr ;
// int (*)(int)
type<int(int)> * b = nullptr ;
// int (*) [5]
type<int [5]> * c = nullptr ;
type<int> *
はint *
型だ。type<int(int)> *
はint(*)(int)
型だ。type<int [5]> *
はint (*) [5]
型だ。これでもう*
をどこに書くかという問題に悩まされることはなくなった。
しかしわざわざtype<T> *
と書くのは依然として面倒だ。T
型は引数で受け取っているのだから、最初からポインターを返してどうだろうか。
template < typename T >
using add_pointer_t = T * ;
さっそく試してみよう。
// int *
add_pointer_t<int> a = nullptr ;
// int **
add_pointer_t<int *> b = nullptr ;
// int(*)(int)
add_pointer_t<int(int)> c = nullptr ;
// int(*)[5]
add_pointer_t<int [5]> d = nullptr ;
どうやら動くようだ。もっと複雑な例も試してみよう。
// int **
add_pointer_t<add_pointer_t<int>> a = nullptr ;
add_pointer_t<int>
はint *
なので、その型をadd_pointer_t<T>
で囲むとその型へのポインターになる。結果としてint **
になる。
ここで実装したadd_pointer_t<T>
はT
がリファレンスのときにエラーになる。
template < typename T > using add_pointer_t = T * ;
// エラー
add_pointer_t<int &> ptr = nullptr ;
実は標準ライブラリにもstd::add_pointer_t<T>
があり、こちらはリファレンスU &
を渡しても、U *
になる。
// OK
// int *
std::add_pointer_t<int &> ptr = nullptr ;
標準ライブラリstd::add_pointer_t<T>
は、T
がリファレンス型の場合、リファレンスは剝がしてポインターを付与するという実装になっている。これをどうやって実装するかについてだが、まだ読者の知識では実装できない。テンプレートについて深く学ぶ必要がある。いまは標準ライブラリに頼っておこう。
標準ライブラリにはほかにも、ポインターを取り除くstd::remove_pointer_t<T>
もある。
// int
std::remove_pointer_t<int * > a = 0 ;
// int
std::remove_pointer_t<
std::add_pointer_t<int>
> b = 0 ;
クラスへのポインターはいままでに学んだものと同じ文法だ。
struct C { } ;
int main()
{
C object ;
C * pointer = &object ;
}
ただし、ポインターを経由してメンバーにアクセスするのが曲者だ。
以下のようなメンバーにアクセスするコードがある。
struct C
{
int data_member ;
void member_function() {}
} ;
int main()
{
C object ;
object.data_member = 0 ;
object.member_function() ;
}
これをポインターを経由して書いてみよう。
以下のように書くとエラーだ。
int main()
{
C object ;
C * pointer = &object ;
// エラー
*pointer.data_member = 0 ;
// エラー
*pointer.member_function() ;
}
この理由は演算子の優先順位の問題だ。上の式は以下のように解釈される。
*(pointer.data_member) = 0 ;
*(pointer.member_function()) ;
ポインターを参照する演算子*
よりも、演算子ドット('.'
)の方が演算子の優先順位が高い。
このような式を可能にする変数pointer
とは以下のようなものだ。
struct Pointer
{
int data = 42 ;
int * data_member = &data ;
int * member_function()
{
return &data ;
}
} ;
int main()
{
Pointer pointer ;
*pointer.data_member = 0;
*pointer.member_function() ;
}
pointer.data_member
はポインターなのでそれに演算子*
を適用して参照した上で0
を代入している。
pointer.member_function()
は関数呼び出しで戻り値としてポインターを返すのでそれに演算子*
を適用している。
演算子*
を先にポインターの値であるpointer
に適用するには、括弧を使う。
(*pointer).data_member = 0 ;
(*pointer).member_function() ;
リファレンスを使ってポインターを参照した結果をリファレンスに束縛して使うこともできる。
C & ref = *pointer ;
ref.data_member = 0 ;
ref.member_function() ;
ただし、ポインターを介してクラスを扱う際に、毎回括弧を使ったりリファレンスを使ったりするのは面倒なので、簡単なシンタックスシュガーとして演算子->
が用意されている。
pointer->data_member = 0 ;
pointer->member_function() ;
a->b
は、(*(a))->b
と同じ意味になる。そのため、上は以下のコードと同じ意味になる。
(*(pointer)).data_member = 0 ;
(*(pointer)).member_function() ;
メンバー関数はクラスのデータメンバーにアクセスできる。このときのデータメンバーはメンバー関数が呼ばれたクラスのオブジェクトのサブオブジェクトになる。
struct C
{
int data { } ;
void set( int n )
{
data = n ;
}
} ;
int main()
{
C a ;
C b ;
// a.dataを変更
a.set(1) ;
// b.dataを変更
b.set(2) ;
}
すでに説明したように、メンバー関数が自分を呼び出したクラスのオブジェクトのサブオブジェクトを参照できるのは、クラスのオブジェクトへの参照を知っているからだ。内部的には以下のような隠し引数を持つコードが生成されたかのような挙動になる。
// コンパイラーが生成するコードのたとえ
struct C
{
int data { } ;
} ;
// 隠し引数
void set( C & obj, int n )
{
obj.data = n ;
}
つまり、メンバー関数は自分を呼び出したクラスのオブジェクトへの参照を知っている。その参照にアクセスする方法がthis
キーワードだ。
this
キーワードはクラスのメンバー関数の中で使うと、メンバー関数を呼び出したクラスのオブジェクトへのポインターとして扱われる。
struct C
{
int data { } ;
void set( int n )
{
// このメンバー関数を呼び出したクラスのオブジェクトへのポインター
C * pointer = this ;
this->data = n ;
}
} ;
先ほど、関数C::set
の中でdata = n ;
と書いたのは、this->data = n ;
と書いたのと同じ意味になる。
this
はリファレンスではなくてポインターだ。この理由は歴史的なものだ。本来ならばリファレンスの方がよいのだが、いまさら変更できないのでポインターになっている。わかりにくければリファレンスに束縛してもよい。
struct S
{
void f()
{
auto & this_ref = *this ;
}
} ;
const
なメンバー関数の中では、this
の型もconst
なクラス型へのポインターになる。
struct S
{
void f()
{
// thisの型はS *
S * pointer = this ;
}
void f() const
{
// thisの型はS const *
S const * pointer = this ;
}
} ;
この理由は、const
なメンバー関数はクラスのオブジェクトへの参照としてconst
なリファレンスを隠し引数として持つからだ。
// コンパイラーが生成するコードのたとえ
struct S { } ;
// 非constなメンバー関数
void f( S & obj ) ;
// constなメンバー関数
void f( S const & obj ) ;
メンバーへのポインターはかなり文法的にややこしい。そもそも、通常のポインターとは概念でも実装でも異なる。
ここで取り扱うのはメンバーへのポインターという概念で、クラスのオブジェクトのサブオブジェクトへのポインターではない。サブオブジェクトへのポインターは通常のポインターと同じだ。
struct Object
{
// サブオブジェクト
int subobject ;
} ;
int main()
{
// クラスのオブジェクト
Object object ;
// サブオブジェクトへのポインター
int * pointer = &object.subobject ;
*pointer = 123 ;
int read = object.subobject ;
}
メンバーへのポインターとは、クラスのデータメンバーやメンバー関数を参照するもので、クラスのオブジェクトとともに使うことでそのデータメンバーやメンバー関数を参照できるものだ。
細かい文法の解説はあとにして例を見せよう。
struct Object
{
int data_member ;
void member_function()
{ std::cout << data_member ; }
} ;
int main()
{
// Object::data_memberメンバーへのポインター
int Object::* int_ptr = &Object::data_member ;
// Object::member_functionメンバーへのポインター
void (Object::* func_ptr)() = &Object::member_function ;
// クラスのオブジェクト
Object object ;
// objectに対するメンバーポインターを介した参照
object.*int_ptr = 123 ;
// objectに対するメンバーポインターを介した参照
// 123
(object.*func_ptr)() ;
// 別のオブジェクト
Object another_object ;
another_object.data_member = 456 ;
// 456
(another_object.*func_ptr)() ;
}
細かい文法はあとで学ぶとして、肝心の機能としてはこうだ。クラスのオブジェクトからは独立したデータメンバーやメンバー関数自体へのポインターを取得する。
struct Object
{
int data_member ;
} ;
// メンバーへのポインター
int Object::*int_ptr = &Object::data_member ;
このポインターをクラスのオブジェクトと組み合わせることで、ポインターが参照するクラスのメンバーで、かつオブジェクトのサブオブジェクトの部分を参照できる。
Object object ;
// メンバーへのポインターをオブジェクトに適用してサブオブジェクトを参照する
object.*int_ptr = 123 ;
では文法の説明に入ろう。
メンバーへのポインターは文法がややこしい。
あるクラス名C
の型名T
のメンバーへのポインター型は以下のようになる。
型名 クラス名::*
T C::*
以下のクラスの各データメンバーへの型はそれぞれコメントのとおりになる。
struct ABC
{
// int ABC::*
int x ;
// int ABC::*
int y ;
// double ABC::*
double d ;
// int * ABC::*
int * ptr ;
} ;
struct DEF
{
// ABC * DEF::*
ABC * abc ;
} ;
順を追って説明していこう。まずクラスABC
のメンバー、
// int ABC::*
int x ;
// int ABC::*
int y ;
このメンバーへのポインターの型はどちらもint ABC::*
になる。データメンバーの型はint
で、クラス名がABC
なので、型名 クラス名::*
に当てはめるとint ABC::*
になる。
// double ABC::*
double d ;
このメンバーへのポインターの型はdouble ABC::*
になる。
最後のクラスABC
のメンバー、
// int * ABC::*
int * ptr ;
これがint * ABC::*
になる理由も、最初に説明した型名 クラス名::*
のルールに従っている。型名がint *
、クラス名がABC
なので、int * ABC::*
だ。
最後の例はクラスDEF
のメンバーとしてクラスABC
のポインター型のメンバーだ。ABC DEF::*
になる。
クラス名C
のメンバー名M
のメンバーへのポインターを得るには以下の文法を使う。
&クラス名::メンバー名
&C::M
具体的な例を見てみよう。
struct C
{
int x = 1 ;
int y = 2 ;
} ;
int main()
{
int C::* x_ptr = &C::x ;
int C::* y_ptr = &C::y ;
C object ;
// 1
std::cout << object.*x_ptr ;
// 2
std::cout << object.*y_ptr ;
}
わかりづらければエイリアス宣言を使うとよい。
using type = int C::* ;
type x_ptr = &C::x ;
あるいはauto
を使うという手もある。
// int C::*
auto x_ptr = &C::x ;
メンバー関数へのポインターは、メンバーへのポインターと関数へのポインターを組み合わせた複雑な文法となるので、とてもわかりづらい。
復習すると、int
型の引数を1つ受け取りint
型の戻り値を返す関数へのポインターの型はint (*)(int)
だ。
int f(int) { return 0 ; }
int (*ptr)(int) = &f ;
この関数がクラスC
のメンバー関数の場合、以下のようになる。
struct C
{
int f(int) { return 0 ; }
} ;
ところで、メンバーへのポインターは型名 クラス名::*
だった。この2つを組み合わせると、以下のように書ける。
struct C
{
int f(int) { return 0 ; }
} ;
int main()
{
// メンバー関数へのポインター
int (C::*ptr)(int) = &C::f ;
// クラスのオブジェクト
C object ;
// オブジェクトを指定したメンバー関数へのポインターを介した関数呼び出し
(object.*ptr)( 123 ) ;
}
メンバー関数へのポインターは難しい。
関数f
の型はint (int)
で、そのポインターの型はint (*)(int)
だ。するとクラス名C
のメンバー関数f
へのポインターの型は、int (C::*)(int)
になる。
メンバー関数へのポインター型の変数を宣言してその値をC::f
へのポインターに初期化しているのが以下の行だ。
// メンバー関数へのポインター
int (C::*ptr)(int) = &C::f ;
このptr
を経由したメンバー関数f
の呼び出し方だが、まずクラスのオブジェクトが必要になるので作る。
C object ;
そして演算子のoperator .*
を使う。
(object.*ptr)(123) ;
object.*ptr
を括弧で囲んでいるのは、演算子の優先順位のためだ。もしこれを以下のように書くと、
object.*ptr(123)
これはptr(123)
という式を評価した結果をメンバーへのポインターと解釈してクラスのオブジェクトを介して参照していることになる。例えば以下のようなコードだ。
struct C { int data { } ; } ;
auto ptr( int ) -> int C::*
{ return &C::data ; }
int main()
{
C object ;
object.*ptr(123) ;
}
演算子の優先順位の問題のために、(object.*ptr)
と括弧で包んで先に評価させ、その後に関数呼び出し式である(123)
を評価させる。
実は演算子operator .*
のほかに、operator ->*
という演算子がある。
.*
はクラスのオブジェクトがリファレンスの場合の演算子だが、->*
はクラスのオブジェクトがポインターの場合の演算子だ。
struct C{ int data { } ; } ;
int main()
{
auto data_ptr = &C::data ;
C object ;
auto c_ptr = &object ;
c_ptr->*data_ptr = 123 ;
}
演算子a->b
が(*(a)).b
となるように、演算子a->*b
も(*(a)).*b
と置き換えられるシンタックスシュガーだ。
上の例で、
c_ptr->*object = 123 ;
は、以下と同じだ。
(*(c_ptr)).*object = 123 ;
.*
や->*
の文法を覚えるのが面倒な場合、標準ライブラリにstd::invoke( f, t1, ... )
という便利な関数が用意されている。
f
がデータメンバーへのポインターで、t1
がクラスのオブジェクトの場合、std::invoke(f, t1)
は以下のような関数になる。
template < typename F, typename T1 >
適切な戻り値の型 std::invoke( F f, T1 t1 )
{
return t1.*f ;
}
なので以下のように書ける。
struct C { int data { } ; } ;
int main()
{
auto data_ptr = &C::data ;
C object ;
// どちらも同じ意味
object.*data_ptr = 123 ;
std::invoke( data_ptr, object ) = 123 ;
}
便利なことにt1
がポインターの場合は、
template < typename F, typename T1 >
適切な戻り値の型 std::invoke( F f, T1 t1 )
{
return (*(t1)).*f ;
}
という関数として振る舞う。そのため、リファレンスでもポインターでも気にせずに使うことができる。
C * c_ptr = &object ;
// どちらも同じ意味
c_ptr->*data_ptr = 123 ;
std::invoke( data_ptr, c_ptr ) = 123 ;
std::invoke
がさらにすごいことに、メンバー関数へのポインターにも対応している。
std::invoke( f, t1, ... )
で、f
がメンバー関数へのポインターで、t1
がクラスのオブジェクトへのリファレンスで、...
が関数呼び出しの際の引数の場合、以下のような関数として振る舞う。
template < typename F, typename T1,
// まだ知らない機能
typename ... Ts >
適切な戻り値の型
invoke( F f, T1 t1,
// まだ知らない機能
Ts ... ts )
{
return (t1.*f)(ts...)
}
厳密にはこの宣言は間違っているのだが、まだ知らない機能を使っているので気にしなくてもよい。大事なことは、std::invoke
の第三引数以降の実引数が、関数呼び出しの実引数として使われるということだ。
struct C
{
int f0() { return 0 ; }
int f1(int) { return 1 ; }
int f2( int, int ) { return 2 ; }
} ;
int main()
{
C object ;
// 同じ
(object.*&C::f0)() ;
std::invoke( &C::f0, object ) ;
// 同じ
(object.*&C::f1)(1) ;
std::invoke( &C::f1, object, 1) ;
// 同じ
(object.*&C::f2)(1,2) ;
std::invoke( &C::f2, object, 1,2) ;
}
この場合も、object
がC
へのリファレンスではなく、C
へのポインターでも自動で認識していいように処理してくれる。