arrayをさらに実装
'std::array'をもっと実装していこう。前章では以下のような簡単な'array'を実装した。
template < typename T, std::size_t N >
struct array
{
T storage[N] ;
T & operator [] ( std::size_t i )
{
return storage[i] ;
}
} ;実はstd::arrayはこのように書かれていない。この章では、'array'の実装を'std::array'に近づけていく。
ネストされた型名
エイリアス宣言を覚えているだろうか。型名に別名を付ける機能だ。
int main()
{
using number = int ;
number x = 123 ;
}エイリアス宣言はクラスの中でも使うことができる。
struct S
{
using number = int ;
number data ;
} ;
int main()
{
S s{123} ;
S::number x = s.data ;
}クラスの中で宣言されたエイリアス宣言による型名を、ネストされた型名という。std::arrayではテンプレート引数を直接使う代わりに、ネストされた型名が使われている。
template < typename T, std::size_t N >
struct array
{
using value_type = T ;
using reference = T & ;
using size_type = std::size_t ;
value_type storage[N] ;
reference operator [] ( size_type i )
{
return storage[i] ;
}
} ;こうすると、T &のようなわかりにくい型ではなくreferenceのようにわかりやすい名前を使える。さらに、クラス外部から使うこともできる。
int main()
{
using array_type = std::array<int, 5> ;
array_type a = {1,2,3,4,5} ;
array_type::value_type x = 0 ;
array_type::reference ref = a[0] ;
}もちろんこれはautoで書くこともできるが、
int main()
{
using array_type = std::array<int, 5> ;
array_type a = {1,2,3,4,5} ;
auto x = 0 ;
auto ref = a[0] ;
}信じられないことに昔のC++にはautoがなかったのだ。その他、さまざまな利点があるのだが、そのすべてを理解するには、まだ読者のC++力が足りない。
要素数の取得: size()
std::array<T,N>にはsize()というメンバー関数がある。要素数を返す。
arrayの場合、Nを返せばよい。
int main()
{
std::array<int, 5> a ;
a.size() ; // 5
std::array<int, 10> b ;
b.size() ; // 10
}さっそく実装しよう。
template < typename T, std::size_t N >
struct array
{
using size_type = std::size_t ;
size_type size() ;
// ... 省略
} ;ここではsizeの宣言だけをしている。
関数は宣言と定義が分割できる。
// 関数の宣言
void f() ;
// 関数の定義
void f() { }メンバー関数も宣言と定義が分割できる。
// クラスの宣言
struct S
{
// メンバー関数の宣言
void f() ;
} ;
// メンバー関数の定義
void S::f() { }メンバー関数の定義をクラス宣言の外で書くには、関数名がどのクラスに属するのかを指定しなければならない。これにはクラス名::を使う。この場合、S::fだ。
メンバー関数のconst修飾
constを付けた変数は値を変更できなくなることはすでに学んだ。
int main()
{
int x = 0 ;
x = 1 ;
int const cx = 0 ;
cx = 0 ; // エラー
}constは変更する必要のない場面でうっかり変更することを防いでくれるとても便利な機能だ。'array'は大きいので関数の引数として渡すときにコピーするのは非効率的だ。なのでコピーを防ぐリファレンスで渡したい。
std::array<T,N>を受け取って要素をすべて出力する関数を書いてみよう。
template < typename Array >
void print( Array & c )
{
for ( std::size_t i = 0 ; i != c.size() ; ++i )
{
std::cout << c[i] ;
}
}
int main()
{
std::array<int, 5> a = {1,2,3,4,5} ;
print( a ) ;
}関数printがテンプレートなのは任意のTとNを使ったstd::array<T,N>を受け取れるようにするためだ。
関数のリファレンスを引数として渡すと、関数の中で変更できてしまう。しかし、上の例のような関数printでは、引数を書き換える必要はない。この関数を使う人間も、引数を勝手に書き換えないことを期待している。この場合、constを付けることで値の変更を防ぐことができる。
template < typename Container >
void print( Container const & c )
{
for ( std::size_t i = 0 ; i != c.size() ; ++i )
{
std::cout << c[i] ;
}
}ではさっそくこれまで実装してきた自作のarrayクラスを使ってみよう。
int main()
{
array<int, 5> a = {1,2,3,4,5} ;
print( a ) ; // エラー
}なぜかエラーになってしまう。
この理由はメンバー関数を呼び出しているからだ。
クラスのメンバー関数はデータメンバーを変更できる。
struct S
{
int data {} ;
void f()
{
++data ;
}
} ;
int main()
{
S s ;
s.f() ; // s.dataを変更
}ということは、const Sはメンバー関数f()を呼び出すことができない。
int main()
{
S s ;
S const & ref = s ;
++ref.data ; // エラー
ref.f() ; // エラー
}ではメンバー関数f()がデータメンバーを変更しなければいいのだろうか。試してみよう。
struct S
{
int data {} ;
void f()
{
// 何もしない
}
} ;
int main()
{
S const s ;
s.f() ; // エラー
}まだエラーになる。この理由を完全に理解するためには、まだ説明していないポインターという機能について学ばなければならない。ポインターの説明はこの次の章で行うとして、いまはさしあたり必要な機能であるメンバー関数のconst修飾を説明する。
constを付けていないメンバー関数をconstなクラスのオブジェクトから呼び出せない理由は、メンバー関数がデータメンバーを変更しない保証がないからだ。その保証を付けるのがメンバー関数のconst修飾だ。
メンバー関数は関数の引数のあと、関数の本体の前にconstを書くことでconst修飾できる。
struct S
{
void f() const
{ }
} ;
int main()
{
S s ;
s.f() ; // OK
S const cs ;
cs.f() ; // OK
}const修飾されたメンバー関数はconstなクラスのオブジェクトからでも呼び出すことができる。
const修飾されたメンバー関数と、const修飾されていないメンバー関数が両方ある場合、クラスのオブジェクトのconstの有無によって適切なメンバー関数が呼び出される。
struct S
{
void f() { } // 1
void f() const { } // 2
} ;
int main()
{
S s ;
s.f() ; // 1
S const cs ;
cs.f() ; // 2
}そしてもう1つ重要なのは、const修飾されたメンバー関数がデータメンバーへのリファレンスを返す場合、
struct S
{
int data {} ;
// データメンバーへのリファレンスを返す
int & get()
{
return data ;
}
} ;const修飾されたメンバー関数は自分のデータメンバーを変更できないので、データメンバーの値を変更可能なリファレンスを返すことはできない。そのため以下のようになる。
struct S
{
int data {} ;
int & get()
{
return data ;
}
// const版
// constリファレンスを返すので変更不可
int const & get() const
{
return data ;
}
} ;自作の'array'のoperator []をconstに対応させよう。'std::array'はconstなリファレンスをconst_referenceというネストされた型名にしている。
template < typename T, std::size_t N >
struct array
{
T storage[N] ;
using reference = T & ;
using const_reference = T const & ;
// 非const版
reference operator [] ( std::size_t i )
{
return storage[i] ;
}
// const版
const_reference operator [] ( std::size_t i ) const
{
return storage[i] ;
}
} ;これでconst arrayにも対応できるようになった。
先頭と末尾の要素:front/back
メンバー関数frontは最初の要素へのリファレンスを返す。backは最後の要素へのリファレンスを返す。
int main()
{
std::array<int, 5> a = {1,2,3,4,5} ;
int & f = a.front() ; // 1
int & b = a.back() ; // 5
}front/backにはreferenceを返すバージョンとconst_referenceを返すバージョンがある。
template < typename T, std::size_t N >
struct array
{
T storage[N] ;
using reference = T & ;
using const_reference = T const & ;
reference front()
{ return storage[0] ; }
const_reference front() const
{ return storage[0] ; }
reference back()
{ return storage[N-1] ; }
const_reference back() const
{ return storage[N-1] ; }
} ;全要素に値を代入: fill
int main()
{
std::array<int, 5> a = {1,2,3,4,5} ;
a.fill(0) ;
// aは{0,0,0,0,0}
}すでにアルゴリズムで実装した'std::fill'と同じだ。
template < typename T, std::size_t N >
struct array
{
T storage[N] ;
void fill( T const & u )
{
for ( std::size_t i = 0 ; i != N ; ++i )
{
storage[i] = u ;
}
}
} ;しかし、せっかくstd::fillがあるのだから以下のように書きたい。
void fill( T const & u )
{
std::fill( begin(), end(), u ) ;
}残念ながらこれは動かない。なぜならば、自作のarrayはまだbegin()/end()とイテレーターに対応していないからだ。これは次の章で学ぶ。