# ポインターの内部実装
ポインターの意味上と文法上の解説は終えた。ここからはポインターの内部実装についてだ。ポインターの値とは外でもない、メモリー上のアドレスのことだ。
# キロバイトとキビバイト
メモリーとアドレスについて解説する前に、キロバイト(Kilo byte)とキビバイト(Kibi byte)の違いについて解説する。
キロ(Kilo)というのはSI接頭語で、$1000^1$を意味する。1キロは1000だ。SI接頭語にはほかにもメガ(Mega, $1000^2$)、ギガ(Giga, $1000^3$)やテラ(Tera, $1000^4$)などの接頭語もある。
長さ1キロメートルは1000メートルで、重さ1キログラムは1000グラムだ。
いま「このCPUのクロック周波数は1GHzだ」と言ったとき、それは$1000^3$Hz = $1000000000$Hzのことだ。
しかし、メモリー容量だけは慣習的に$1000^n$ではなく、$1024^n$を使う。
一般人が「このメモリーは1KBだ」と言ったとき、それは1024バイトのことだ。1GBのメモリーは$1024^3 バイト = 1073741824 バイト$だ。筆者が本書を執筆するのに使ったラップトップコンピューターは32GBのメモリーを積んでいるがこれは34359738368バイトだ。
メモリーの容量が10進数ではなく2進数で数えられているのは、メモリーは2進数で扱うのがハードウェア的に都合がいいからだ。そのため、慣習的にキロは$1000^1$ではなく$1024^1$を意味するようになってしまった。
このため、IEEE 1541では10進SI接頭語と対になる2進接頭語を定義した。
接頭語 値
キビ(kibi, Ki) $2^{10}$ メビ(mebi, Mi) $2^{20}$ ギビ(gibi, Gi) $2^{30}$ テビ(tebi, Ti) $2^{40}$ ペビ(pebi, Pi) $2^{50}$ エクスビ(exbi, Ei) $2^{60}$
本書では1KBは1000バイトで、1KiBが1024バイトを意味する。
# メモリーとアドレス
コンピューターにはメモリーやストレージと呼ばれる記憶領域がある。情報の最小単位はすでに学んだようにビットだが、情報をビット単位で扱うのは不便なので、慣習的に複数の連続したビットを束ねたバイトという単位で扱っている。1バイトはほとんどのアーキテクチャで8ビットだ。メモリーは複数の連続したバイト列で成り立っている。
この連続したバイト列の中の任意の1バイトを指し示すのがアドレスだ。メモリーのバイト列の最初の1バイトのアドレスを0とし、次の1バイトアドレスを1とし、以降、その次を前のアドレスに1加えた値にしてみよう。
そのようなメモリーとアドレスのコンピューターでは、1バイトの符号なし整数で表現されたアドレスは、256バイトのメモリーの中の任意の1バイトをアドレスとして参照することができる。
これはとても抽象化された計算機で、現実の計算機はもっと複雑な実装になっている。しかしC++の規格としては、メモリーとはフラットな連続したバイト列であって、その任意の各バイトをアドレスから参照可能だという想定になっている。
アドレスが1バイトの符号なし整数で表現され、そのすべてのビットが使われる場合、256バイトの連続したメモリーをアドレス可能だ。
アドレスが2バイトならば、64KiBのメモリーをアドレス可能だ。
アドレスが4バイトならば、4GiBのメモリーをアドレス可能だ。
アドレスが8バイトならば、16EiBのメモリーをアドレス可能だ。
ポインターの値というのは、このアドレスの値のことだ。
# ポインターのサイズ
ポインターの値というのはアドレスの値だ。ポインターの値を格納するのにもメモリーが必要だ。ではポインターのサイズは何バイトあるのだろう。
型T
のサイズを調べるにはsizeof(T)
を使う。
template <typename T >
void print_size()
{
std::cout << sizeof(T) << "\n"s ;
}
int main()
{
print_size<int *>() ;
print_size<double *>() ;
// ポインターへのポインター
print_size<int **>() ;
}
筆者の環境でこのプログラムを実行した結果は以下のようになった。
8
8
8
どうやら筆者の環境ではポインターのサイズはすべて8バイトらしい。
# ポインターの値
ポインターが8バイト、つまり64ビットの値であるならば、それを8バイトの符号なし整数として解釈した値はどうなるのだろう。
C++にはすべてのポインターの値を格納できるサイズの符号なし整数型が用意されている。std::uintptr_t
だ。
int main()
{
std::cout << sizeof( std::uintptr_t ) ;
}
筆者の環境でこのプログラムを実行した結果も8
が出力される。
ポインターもstd::uintptr_t
も8バイトだ。ポインターのバイト列をstd::uintptr_t
として強引に解釈すれば、符号なし整数としての値を出力してみよう。
ある値from
のバイト列を、同じバイト数のある型to
の値として強引に解釈するC++20で追加された標準ライブラリに、std::bit_cast<to>(from)
がある。
#include <bit>
int main()
{
int data {} ;
std::cout << std::bit_cast<std::uintptr_t>(&data) ;
}
このプログラムを何度か実行した結果、以下のような結果を得た。
$ make run
140725678382588
$ make run
140721510940268
$ make run
140731669632396
私の環境ではポインターの具体的な値は実行ごとに異なる。これは私の使っているOSがASLR(Address Space Layout Randomization)を実装しているためだ。興味のある読者は調べてみるとよい。
この値はint
型の変数data
のポインターの整数としての値だ。このアドレスの場所に、int
型のオブジェクトの最初の1バイトがあり、その次の場所に次の1バイトがある。
筆者の環境ではint
型は4バイトだ。
int main()
{
std::cout << sizeof(int) ;
}
int
型のオブジェクトは4バイトの連続したメモリー上に構築されている。つまり、本質的には以下のようなコードと同等になる。
int main()
{
std::byte data[4] ;
std::cout << std::bit_cast<std::uintptr_t>(&data[0]) ;
}
std::byte
というのはsizeof(std::byte)
の結果が1になる、サイズが1バイトの符号なし整数型だ。
std::byte
はC++で1バイトの生の値を表現するために使うことができる。配列は連続したバイト列なので、4バイトのint
型は、本質的には上のようなコードになる。ただし上のコードはアライメントという概念が欠けている。これについてはあとで説明する。
ところで、std::bit_cast
は2020年に制定される国際標準規格C++20から入った。しかるに筆者がこの文章を書いているのは2018年だ。まだC++20を完全に実装したC++コンパイラーは存在しない。この本が出版されてしばらくは、読者の手元にもC++20コンパイラーは存在しないだろう。
# std::bit_cast
の実装
ないものは自分で実装すればいい。std::bit_cast
に近いものを実装してみよう。
今回実装するbit_cast
は以下のような関数テンプレートだ。
template < typename To, typename From >
To bit_cast( From const & from )
{
// 値fromのバイト列をTo型の値として解釈して返す。
}
bit_cast
の実装にはポインターが必要だ。From
の値を表現するバイト列への先頭のポインターを取り、バイト単位でTo
の値を表現するバイト列にコピーすればよい。
標準ライブラリにはそのような処理を行ってくれるstd::memcpy(dest, src, n)
がある。ポインターsrc
からn
バイトをポインターdest
からn
バイトに書き込む関数だ。
template < typename To, typename From >
To bit_cast( From const & from )
{
To to ;
std::memcpy( &to, &from, sizeof(To) ) ;
return to ;
}
これでstd::bit_cast
の実装はできた。しかしこの実装は問題をstd::memcpy
にたらい回しにしただけだ。std::memcpy
も実装できて初めてstd::bit_cast
を自前で実装できたと言える。
# std::memcpy
の実装
std::memcpy
はC++コンパイラーによって効率のよいコードに置き換えられる。そのため自分で実装したstd::memcpy
を標準ライブラリと同じ効率にすることは難しいが、機能的にはほとんど同じものを作ることができる。
memcpy
の実装にはポインターの詳細な理解が必要だ。
std::memcpy
関数は以下のようになっている。
void * memcpy( void * dest, void const * src, std::size_t n )
{
// srcの先頭バイトからnバイトを
// destの先頭バイトからのバイト列にコピーし
// destを返す
}
見慣れないvoid *
という型が出てきた。まずはこれについて学ぼう。
# void型
void
は特別な型だ。void
型は何も値を持たない型という意味を持つ。例えば関数が戻り値を何も返さない場合、void
型を返す関数として宣言される。
// 何も値を返さない関数
void f()
{
// 何も値を返さない
return ;
}
あらゆる値はvoid
型に変換することができる。変換した結果は、何も値を持たない。
void f()
{
return static_cast<void>(123) ;
}
C++17では、void
型の変数は作れない。
// エラー
void x ;
ところで、読者が本書を読むころには、C++規格ではvoid
型の変数が作れるようになっているかもしれない。これはvoid
型だけ変数を作れないのが面倒だから作れるようになるだけで、具体的な値のない変数になる。
# void *型
void *
型は「void
型へのポインター型」だ。int *
が「int
型へのポインター型」であるのと同じだ。
void *
型の値は、ある型T
へのポインター型から型T
という情報が消え去ったポインターの値だ。ポインターの値というのはアドレスで、アドレスというのは単なるバイト単位のメモリーを指す整数値だということを学んだ。void *
型は特定の型を意味しないポインター型だ。
ある型T
へのポインター型の値は、void *
型に変換できる。
int main()
{
int data { } ;
// int *からvoid *への変換
void * ptr = &data ;
}
void *
型の値e
から元の型T
へのポインターに変換するにはstatic_cast<T *>(e)
が必要だ。
int main()
{
int data { } ;
void * void_ptr = &data ;
int * int_ptr = static_cast<int *>(void_ptr) ;
}
もしstatic_cast<T *>(e)
のe
がT *
として妥当なアドレスの値であれば、変換後も正しく動く。
T const *
型はvoid const *
型に変換できる。その逆変換もできる。
int main()
{
int data {} ;
int const * int_const_ptr = &data ;
void const * void_const_ptr = int_const_ptr ;
int const * original = static_cast<int const *>(void_const_ptr) ;
}
ポインター間の型変換でconst
を消すことはできない。
memcpy
はvoid *
を使うことで、どんなポインターの値でも取れるようにしている。C++にはテンプレートがあるので以下のように宣言してもよいのだが、
template < typename Dest, typename Src >
Dest * memcpy( Dest * dest, Src const * src, std::size_t n ) ;
memcpy
はC++以前からあるC言語ライブラリなので、こうなっている。
# std::byte型
void *
型はアドレスだけを意味するポインター型なので、参照することができない。memcpy
の実装にはポインターを経由して参照先を1バイトずつ読み書きする必要がある。そのための型としてstd::byte
がある。
std::byte
型は1バイトを表現するための型だ。sizeof(std::byte)
の結果は1
になる。
1バイトというのは10進数で$0 \leqq n \leqq 255$までの値を扱う。
std::byte
はとても厳格に1バイトの符号なし整数として振る舞うので、普通の整数で初期化や代入をすることができない。
// エラー
std::byte a = 123 ;
std::byte b(123) ;
// これもエラー
a = 123 ;
std::byte
に具体的な値で初期化するには{x}
を使う。
std::byte a{123} ;
std::byte
に値を代入するにはstd::byte{x}
を使う
std::byte a ;
a = std::byte{123} ;
static_cast<std::byte>(x)
やstd::byte(x)
はコンパイルできるが、使ってはならない。
// 使ってはならない
std::byte a = static_cast<std::byte>(123) ;
std::byte b = std::byte(123) ;
なぜ使ってはならないかというと、範囲外の値を無理やり変換してしまうからだ。
std::byte a = static_cast<std::byte>(256) ;
std::byte b = std::byte(-1) ;
# 配列のメモリー上での表現
配列は要素型を表現するバイト列をメモリー上に連続して配置する。
例えばint [3]
という配列があり、sizeof(int)
が4
の場合、全体で12バイトのメモリーが確保される。
int data[3] = {1,2,3} ;
最初の4バイト(0バイト目から3バイトまで)の領域は0番目の要素であるdata[0]
で、その値は1
だ。
次の4バイト(4バイト目から7バイト目まで)の領域は1番目の要素であるdata[1]
で、その値は2
だ。
最後の4バイト(8バイト目から11バイト目まで)の領域は2番目の要素であるdata[2]
で、その値は3
だ。
TODO: メモリーの図示
↓最初の4バイト
<----->
□-□-□-□-□-□-□-□-□-□-□-□
<----->
↑次の4バイト
<----->
↑最後の4バイト
fig/fig30-01.png
実際にアドレスの生の値を出力して確かめてみよう。
// 生のアドレスを出力する関数
template < typename T >
void print_raw_address( T ptr )
{
std::cout << std::bit_cast<std::uintptr_t>(ptr) << "\n"s ;
}
int main()
{
int data[3] = {0,1,2} ;
print_raw_address( &data[0] ) ;
print_raw_address( &data[1] ) ;
print_raw_address( &data[2] ) ;
}
このプログラムを筆者の環境で実行すると以下のように出力された。
140736120015884
140736120015888
140736120015892
筆者の環境ではsizeof(int)
は4だ。&data[0]
の生のアドレスに4を足した値が&data[1]
になっていることがわかる。
# ポインターと整数の演算
ポインターと整数を加減算することができる。
ポインターT *
に整数n
を足すと、ポインターのアドレスがsizeof(T) * n
加算される。この結果、ポインターは要素が配列のように配置された場合にn
個先の要素を指すようになる。
template < typename T >
void print_raw_address( T ptr )
{
std::cout << std::bit_cast<std::uintptr_t>(ptr) << "\n"s ;
}
int main()
{
int a[4] = {0,1,2,3} ;
// 0個目の要素へのポインター
int * a0 = &a[0] ;
print_raw_address( a0 ) ;
// アドレスがsizeof(int) * 3加算される
// a3は3個目の要素へのポインター
int * a3 = a0 + 3 ;
print_raw_address( a3 ) ;
// アドレスがsizeof(int) * 2減算される。
// a1は1個目の要素へのポインター
int * a1 = a3 - 2 ;
print_raw_address( a1 ) ;
}
これを筆者の環境で実行すると以下のように出力された。
140722117900224
140722117900236
140722117900228
最初の値がa0
, 次の値がa3
, 最後の値がa1
だ。
筆者の環境ではsizeof(int)
は4
だ。するとa3
の値はa0
の値より12多い値になっているはずだ。実際にそうなっている。a1
はa3
に対して8少ない値になっているはずだ。実際にそうなっている。
# いよいよmemcpyの実装
これまで学んできたことをすべて使い、ようやくmemcpy
が実装できる。
dest
をstd::byte *
型に変換するsrc
をstd::byte const *
型に変換するsrc
の参照先からn
バイトをdest
の参照先にコピーするdest
を返す
void * memcpy( void * dest, void const * src, std::size_t n )
{
// destをstd::byte *型に変換
auto d = static_cast<std::byte *>(dest) ;
// srcをstd::byte const *型に変換する
auto s = static_cast<std::byte const *>(src) ;
// srcからnバイトコピーするのでnバイト先のアドレスを得る
auto last = s + n ;
// nバイトコピーする
while ( s != last )
{
*d = *s ;
++d ;
++s ;
}
// destを返す
return dest ;
}
# memcpyの別の実装
ポインターはoperator []
に対応している。
ポインターp
と整数i
に対してp[i]
と書いたとき、*(p + i)
という意味になる。
int main()
{
int a[5] = {0,1,2,3,4} ;
int * p = &a[0] ;
p[0] ; // 0
p[2] ; // 2
int * p2 = &p[2] ;
p2[1] ; // 3
}
memcpy
はoperator []
を使って書くこともできる。
void * memcpy( void * dest, void const * src, std::size_t n )
{
auto d = static_cast<std::byte *>(dest) ;
auto s = static_cast<std::byte const *>(src) ;
for ( std::size_t i = 0 ; i != n ; ++i )
{
d[i] = s[i] ;
}
return dest ;
}
# データメンバーへのポインターの内部実装
データメンバーへのポインターの整数としての値は少し変わっている。
ポインターの生の値は、メモリー上で値を表現しているバイト列の先頭アドレスだ。
データメンバーへのポインターは、具体的なクラスのオブジェクトへのポインターやリファレンスがあって初めて意味がある。
struct S { int x = 123 ; } ;
int main()
{
int data = 123 ;
int * ptr = &data ;
// ptr単体で参照できる
int read1 = *ptr ;
S object ;
int S::* mem_ptr = &S::x ;
// objectとmem_ptrの2つで参照できる
int read2 = object.*mem_ptr ;
}
配列が要素型のバイト列を連続して配置したメモリーレイアウトをしているように、クラスもデータメンバーを連続して配置したメモリーレイアウトをしている。
たとえば以下のようなクラスObject
がある場合、
struct Object
{
int x ;
int y ;
int z ;
} ;
このクラスのサイズはsizeof(Object)
だ。このクラスはint
型のサブオブジェクトを3つ持っているので、そのサイズは少なくともsize(int)*3
はある。
実際に確かめてみよう。
struct Object
{
int x ;
int y ;
int z ;
} ;
int main()
{
std::cout << "sizeof(int): " << sizeof(int) << "\n"s ;
std::cout << "sizeof(Object): " << sizeof(Object) << "\n"s ;
}
このプログラムを筆者の環境で実行すると以下のように出力された。
sizeof(int): 4
sizeof(Object): 12
int
型のサイズが4
で、Object
型のサイズが12
ということは、クラスObject
にはint
型のサブオブジェクトが3つ、隙間なく連続して配置されているということだ。すべてのクラスがこうではないが、今回の私の環境ではそうなっている。
全体で12バイトということは、配列int [3]
と同じように、最初の4バイトにx
, y
, z
のどれかが、次の4バイトに残りのどちらかが、最後の4バイトに残りが配置されている。
データメンバーへのポインターというのは、このクラスのオブジェクトを表現するバイト列の先頭から何バイト目に配置されているかというオフセット値になっている。
具体的な値を見てみよう。
template < typename T >
void print_raw_address( T ptr )
{
std::cout << bit_cast<std::uintptr_t>(ptr) << "\n"s ;
}
struct Object
{
int x ;
int y ;
int z ;
} ;
int main()
{
print_raw_address( &Object::x ) ;
print_raw_address( &Object::y ) ;
print_raw_address( &Object::z ) ;
}
このプログラムを筆者の環境で実行すると以下のように出力される。
0
4
8
筆者の環境では、x
はクラスの先頭アドレスからオフセット0バイトに、y
はオフセット4バイトに、z
はオフセット8バイトに配置されているようだ。
確かめてみよう。
struct Object
{
int x = 123 ;
int y = 456 ;
int z = 789 ;
} ;
int main()
{
Object object ;
// クラスのオブジェクトの先頭アドレス
std::byte * start = bit_cast<std::byte *>(&object) ;
// オフセット0
int * x = bit_cast<int *>(start + 0) ;
// オフセット4
int * y = bit_cast<int *>(start + 4) ;
// オフセット8
int * z = bit_cast<int *>(start + 8) ;
std::cout << *x << *y << *z ;
}
筆者の環境では以下のように出力される
123456789
このプログラムの実行結果は環境によって変わる。読者の使っている環境でデータメンバーへのポインターが筆者の環境と同じように実装されているとは限らない。