Skip to content

Files

Latest commit

3d9f37a · Aug 29, 2019

History

History
405 lines (312 loc) · 8.92 KB

023-template.md

File metadata and controls

405 lines (312 loc) · 8.92 KB

テンプレート

問題点

前章で我々は'std::array'のようなものを実装した。C++を何も知らなかった我々がとうとうクールなキッズは皆やっているというクラスを書くことができた。素晴らしい成果だ。

しかし、我々の書いた'array_int_10''std::array'とは異なる。

// 標準ライブラリ
std::array<int, 10> a ;
// 我々のクラス
array_int_10 a ;

もし要素数を20個にしたければarray_int_20を新たに書かなければならない。するとarray_int_1とかarray_int_10000のようなクラスを無数に書かなければならないのだろうか。要素の型をdoubleにしたければarray_double_10が必要だ。

しかし、そのようなクラスはほとんど同じような退屈な記述の羅列になる。

struct array_int_1
{
    int storage[1] ;
    int & operator []( std::size_t i )
    { return storage[i] ; }
} ;

// array_int_2, array_int_3, ...

struct array_int_10000
{
    int storage[10000] ;
    int & operator []( std::size_t i )
    { return storage[i] ; }
} ;

struct array_double_1
{
    double storage[1] ;
    double & operator []( std::size_t i )
    { return storage[i] ; }
} ;

// array_double_2, array_double_3, ...

これは怠惰で短気なプログラマーには耐えられない作業だ。C++にはこのような退屈なコードを書かなくても済む機能がある。しかしその前に、引数について考えてみよう。

関数の引数

1を2倍する関数を考えよう。

int one_twice()
{
    return 1 * 2 ;
}

上出来だ。では2を2倍する関数を考えよう。

int two_twice()
{
    return 2 * 2 ;
}

素晴らしい。では3を2倍する関数、4を2倍する関数...と考えていこう。

ここまで読んでthree_twicefour_twiceを思い浮かべた読者にはプログラマーに備わるべき美徳が欠けている。怠惰で短気で傲慢なプログラマーはそんなコードを書かない。引数を使う。

int twice( int n )
{
    return n * 2 ;
}

具体的な値を2倍する関数を値の数だけ書くのは面倒だ。具体的な値は定めず、引数で外部から受け取る。そして引数を2倍して返す。引数は汎用的なコードを任意の値に対して対応させるための機能だ。

関数のテンプレート引数

twiceをさまざまな型に対応させるにはどうすればいいだろう。例えばint型とdouble型に対応させてみよう。

int twice( int n )
{
    return n * 2 ;
}

double twice( double n )
{
    return n * 2.0 ;
}

整数型にはintのほかにも、short, long, long longといった型がある。浮動小数点数型にはfloatlong doubleもある。ということは以下のような関数も必要だ。

short twice( short n )
{
    return n * 2 ;
}

long twice( long n )
{
    return n * 2 ;
}

long long twice( long long n )
{
    return n * 2 ;
}

float twice( float n )
{
    return n * 2 ;
}

long double twice( long double n )
{
    return n * 2 ;
}

ところで、整数型には符号付きと符号なしの2種類があるということは覚えているだろうか?

int twice( int n )
{
    return n * 2 ;
}

unsigned int twice( unsigned int n )
{
    return n * 2 ;
}

// short, long, long longに対しても同様

C++ではユーザーが整数型のように振る舞うクラスを作ることができる。整数型を複数使って巨大な整数を表現できるクラスも作ることができる。

// 多倍長整数クラス
// unsigned long longが256個分の整数の実装
struct bigint
{
    unsigned long long storage[256] ;
} ;

bigint operator * ( bigint const & right, int )
{
    return // 実装
}

このクラスに対応するには当然、以下のように書かなければならない。

bigint twice( bigint n )
{
    return n * 2 ;
}

そろそろ怠惰と短気を美徳とするプログラマー読者は耐えられなくなってきただろう。これまでのコードは、単にある型Tに対して、

T twice( T n )
{
    return n * 2 ;
}

と書いているだけだ。型Tがコピーとoperator *(T, int)に対応していればいい。型Tの具体的な型について知る必要はない。

関数が具体的な値を知らなくても引数によって汎用的なコードを書けるように、具体的な型を知らなくても汎用的なコードを書けるようになりたい。その怠惰と短気に答えるのがテンプレートだ。

テンプレート

通常の関数が値を引数に取ることができるように、テンプレートは型を引数に取ることができる。

テンプレートは以下のように宣言する。

template < typename T >
    宣言 

テンプレートを関数に使う関数テンプレートは以下のように書く。

template < typename T >
T twice( T n )
{
    return n * 2 ;
}

int main()
{
    twice( 123 ) ;  // int
    twice( 1.23 ) ; // double 
}

template < typename T >は型Tテンプレート引数に取る。テンプレートを使った宣言の中では、Tが型として扱える。

template < typename T >
T f( T n )
{
    T x = n ;
}

関数引数を取るように、テンプレートテンプレート引数を取る。

// テンプレートはテンプレート引数template_parameterを取る
template < typename template_parameter >
// 関数は引数function_parameterを取る
// 引数の型はtemplate_parameter
void f( template_parameter function_parameter )
{
}

テンプレートが「使われる」ときに、テンプレート引数に対する具体的な型が決定する。

template < typename T >
void f( T const & x )
{
    std::cout << x ;
}

int main()
{
    // Tはint
    f( 0 ) ;
    // Tはdouble
    f( 0.0 ) ;
    // Tはstd::string
    f( "hello"s ) ;
}

テンプレートを使うときに自動でテンプレート引数を推定してくれるが、<T>を使うことで明示的にテンプレート引数T型に指定することもできる。

template < typename T >
void f( T const & x )
{
    std::cout << x ;
}

int main()
{
    // Tはint
    f<int>(0) ;

    // Tはdouble
    // int型0からdouble型0.0への変換が行われる
    f<double>( 0 ) ;
}

テンプレート引数は型ではなく整数型の値を渡すこともできる。

template < int N >
void f()
{
    std::cout << N ;
}

int main()
{
    // Nは0
    f<0>() ;
    // Nは123
    f<123>() ;
}

ただし、テンプレート引数はコンパイル時にすべてが決定される。なのでテンプレート引数に渡せる値はコンパイル時に決定できるものでなければならない。

template < int N >
void f() { }

int main()
{
    // OK
    f<1+1>() ;

    int x{} ;
    std::cin >> x ;
    // エラー
    f<x>() ;
}

テンプレート引数がコンパイル時に決定されるということは、配列のサイズのようなコンパイル時に決定されなければならない場面でも使えるということだ。

template < std::size_t N >
void f()
{
    int buffer[N] ;
}

int main()
{
    // 配列bufferのサイズは10
    f<10>() ;
    // サイズは12
    f<12>() ;
}

テンプレートを使ったコードは、与えられたテンプレート引数に対して妥当でなければならない。

template < typename vec >
void f( vec & v )
{
    v.push_back(0) ;
}

int main()
{
    std::vector<int> a ;
    // OK
    f( a ) ;
    std::vector<double> b ;
    // OK
    // intからdoubleへの変換
    f( b ) ;

    std::vector<std::string> c ;
    // エラー
    // intからstd::stringに変換はできない
    f( c ) ;

    // エラー
    // int型はメンバー関数push_backを持っていない
    f( 0 ) ;
}

クラステンプレート

テンプレートクラスにも使える。関数テンプレート関数の前にテンプレートを書くように、

template < typename T > // テンプレート
void f( ) ; // 関数

クラステンプレートクラスの前にテンプレートを書く。

template < typename T > // テンプレート
struct S { } ; // クラス

関数の中でテンプレート引数名を型や値として使えるように、

template < typename T, T N >
T value()
{
    return N :
}

int main()
{
    value<int, 1>() ;
    value<short, 1>() ;
}

クラスの中でもテンプレート引数名を型や値として使える。

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    T & operator [] ( std::size_t i )
    {
        return storage[i] ;
    }
} ;

なんと、もう'std::array'が完成してしまった。