関数テンプレート | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、関数テンプレートを取り上げます。関数テンプレートは、関数のテンプレート(雛形)を作る機能で、特定の型に依存しない関数を実現できます。クラスやクラステンプレート内に宣言されるメンバ関数テンプレートや、変数テンプレート、特定の型に依存しないラムダ式であるジェネリックラムダについても取り上げます。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



関数テンプレート 🔗

前のページで取り上げたクラステンプレートはクラスの雛形を作る機能でした。次に取り上げるのは、関数の雛形を作る関数テンプレート (function template) です。テンプレート関数 (function template) と呼ぶこともあります。

基本的な考え方はクラステンプレートと同じで、関数の実装コードの一部分を空欄にしておいて、使う側で空欄部分を埋めるということです。実装内の空欄部分を表現するものがテンプレート仮引数、そこを埋める情報がテンプレート実引数でした(「クラステンプレート」のページを参照)。

クラステンプレートをより活用しやすくするためにも関数テンプレートが活用できます。たとえば、 テンプレート仮引数を残したまま、引数に渡すような関数は作成できません。

// std::vector<T> から RingBuffer<T> にコピーする関数。
// テンプレート仮引数残ったままの関数は作成できない。
mylib::RingBuffer<T>& copy_vector_to_ringbuffer(const std::vector<T>& vec, mylib::RingBuffer<T>& ringbuf)
{
    for (const auto& v : vec) {
        ringbuf.push_back(v);
    }
    return ringbuf;
}

このような関数は、関数テンプレートを使えば実現できます(このあと登場します)。

標準ライブラリにある関数テンプレートとしては、std::find関数(「要素を探索する」のページを参照)や std::copy関数(「配列」のページを参照)などが挙げられます。関数テンプレートになっているおかげで、検索やコピーを行う対象の型が1つに限定されることなく使用できます。

標準ライブラリでは、std::vector や std::deque のようなデータ構造を表現しているクラステンプレートをコンテナと呼んでいるように(「queue、priority_queue、deque」のページを参照)、std::find や std::copy のようなコンテナをターゲットにして処理を行う関数テンプレートたちをアルゴリズム (algorithm) あるいは STLアルゴリズム (Standard Template Library algorithm) と呼んでいます。

関数テンプレートを定義する 🔗

関数テンプレートは、通常の関数の宣言や定義の手前に、templateキーワードと「<テンプレート仮引数の宣言の並び>」を置くことで宣言、定義が行えます。

// 宣言
template <テンプレート仮引数の宣言の並び>
戻り値の型 関数テンプレートの名前(仮引数の並び);

// 定義
template <テンプレート仮引数の宣言の並び>
戻り値の型 関数テンプレートの名前(仮引数の並び)
{
    本体のコード
}

戻り値の型を後ろに書く構文でも構いません(「関数を作る」のページを参照)。

// 宣言
template <テンプレート仮引数の宣言の並び>
auto 関数テンプレートの名前(仮引数の並び) -> 戻り値の型;

// 定義
template <テンプレート仮引数の宣言の並び>
auto 関数テンプレートの名前(仮引数の並び) -> 戻り値の型;
{
    本体のコード
}

戻り値の型を推論させる記述でも構いません(「関数から値を返す」のページを参照)。

// 宣言
template <テンプレート仮引数の宣言の並び>
auto 関数テンプレートの名前(仮引数の並び);

// 定義
template <テンプレート仮引数の宣言の並び>
auto 関数テンプレートの名前(仮引数の並び);
{
    本体のコード
}

【C++20】template <テンプレート仮引数の宣言の並び> を記述せず、関数の仮引数の型を auto にすることで、関数テンプレートを簡易的に定義する機能が加わりました。たとえば、auto f(auto a, auto b); のような記述が可能になっています。[1]

「テンプレート仮引数の宣言の並び」には1つ以上のテンプレート仮引数を記述します。2つ以上あるときは , で区切ります。1つ分のテンプレート仮引数は次のいずれかの方法で記述します。

typename 名前
class 名前

typenameキーワード と classキーワードはどちらを使ってもまったく同じです。ここでは struct は使えません。「名前」がテンプレート仮引数の名前です。あまりないことですが、名前をどこにも使わないのなら指定を省略できます。テンプレート仮引数の名前は、その関数テンプレートでのみ使用できます。

次のコードは、2つの引数を加算して返す関数テンプレートの例です。

template <typename T>
T add(T a, T b)
{
    return a + b;
}

クラステンプレートのメンバ関数と同様(「クラステンプレート」のページを参照)、関数テンプレートを使用する側から、その関数テンプレートの定義が見えている必要があります。そのため、関数テンプレートの宣言をヘッダファイルに公開するのであれば、定義も同じヘッダファイルに書く(あるいは定義だけにする)ことが多いです。

関数テンプレートの定義を別ファイルに分けたい場合は、別のヘッダファイルを作ってそちらに定義だけを置く方法を取ることもあります。結果的に呼び出し側から見えるところに定義があれば大丈夫です。

次のコードは、std::vector<T> の要素を RingBuffer<T> にコピーする関数テンプレートの例です。RingBuffer は「クラステンプレート」のページで作ったものです。

template <typename T>
mylib::RingBuffer<T>& copy_vector_to_ringbuffer(const std::vector<T>& vec, mylib::RingBuffer<T>& ringbuf)
{
    for (const auto& v : vec) {
        ringbuf.push_back(v);
    }
    return ringbuf;
}

このように、クラステンプレートのテンプレート実引数の部分に、関数テンプレートのテンプレート仮引数を置くように実装すれば、任意の要素型の std::vector と、同じ要素型の mylib::RingBuffer を対象にした関数が実現できます。もう1つテンプレート仮引数を追加して、std::vector<T> から mylib::RingBuffer<U> にコピーするように作ることも可能です。

次のように実装した場合は、コンテナの種類ごと取り換え可能な、より汎用性が高い関数テンプレートになります。

template <typename SrcContainer, typename DestContainer>
DestContainer& copy_container_elements(const SrcContainer& src, DestContainer& dest)
{
    for (const auto& v : src) {
        dest.push_back(v);
    }
    return dest;
}

ただし、src に対して範囲for文が使えることや、dest に呼び出し可能な push_backメンバ関数がなければコンパイルできません。また、両者の要素型を一定させなければならない制約がないかのようにもみえますが、実際には dest.push_back(v) のところで、型が適切に変換可能でなければなりません。

テンプレートのインスタンス化 🔗

関数テンプレートを通常の関数のように使うためには、空欄部分(テンプレート仮引数)を埋める情報、つまりはテンプレート実引数を与える必要があります。これが関数テンプレートにおける、テンプレートのインスタンス化(実体化、具現化) (template instantiation) です。生成された実体(関数)は(テンプレートの)特殊化 (specialization) と呼ばれます。

【上級】ここで取り上げるような特殊化は暗黙的特殊化 (implicit specialization) と呼ばれます。このほかに、専用の構文を使ってプログラマーが指示を与えて行う明示的特殊化 (explicit specialization) があります[2]。特殊化についての詳細は、ページを改めて取り上げます。

#include <iostream>

template <typename T>
T add(T a, T b)
{
    return a + b;
}

int main()
{
    auto r1 = add<int>(10, 20);      // T に int を指定してインスタンス化し、呼び出している
    auto r2 = add<double>(5.5, 3.0); // T に double を指定してインスタンス化し、呼び出している
    std::cout << r1 << "\n";
    std::cout << r2 << "\n";
}

実行結果:

30
8.5

関数テンプレートの実引数は、関数テンプレート名の直後に <> で囲って書きます。add<int> のような、テンプレートの名前とテンプレート実引数を並べた記述をテンプレートID (template-id) と呼びます。後述するテンプレート実引数推論デフォルトテンプレート実引数があるため、<> の内側は省略されることがあります。

関数テンプレート内のテンプレート仮引数が、指定したテンプレート実引数によって置き換えられます。int と double を指定してインスタンス化したとすると、次のように2つの特殊化が生成されます。

// int の場合
int add(int a, int b)
{
    return a + b;
}

// double の場合
double add(double a, double b)
{
    return a + b;
}

このように別々の特殊化された関数が存在することになるので、静的ローカル変数(「メモリとオブジェクト」のページを参照)を宣言した場合は、それぞれに別個のものが作られます[3]

テンプレート実引数推論 🔗

関数テンプレートでは、(関数に渡すほうの)実引数から、テンプレート実引数を推論させることが可能です。この機能をテンプレート実引数推論 (template argument deduction) と呼びます[4]。テンプレート実引数推論を行うには、推論させるテンプレート実引数の明示的な記入を省略します。結果、<> の内側が空になるときは <> ごと省略できます。

#include <iostream>

template <typename T>
T add(T a, T b)
{
    return a + b;
}

int main()
{
    auto r1 = add(10, 20);    // 10 と 20 から int を推論。T == int
    auto r2 = add(5.5, 3.0);  // 5.5 と 3.0 から double を推論。T == double
    std::cout << r1 << "\n";
    std::cout << r2 << "\n";
}

実行結果:

30
8.5

【C++17】クラステンプレートの場合も、コンストラクタの実引数からテンプレート仮引数を推論する機能が加わりました[5]

テンプレート仮引数が複数ある場合、1つ目のテンプレート実引数から明示的に指定し、途中からテンプレート実引数推論に任せることも可能です。

template <typename T1, typename T2, typename T3>
void f(T1 a, T2 b, T3 c);

f<int>(10, 10.5, 'x');          // T1 は明示。T2、T3 はテンプレート実引数推論
f<int, double>(10, 10.5, 'x');  // T1、T2 は明示。T3 はテンプレート実引数推論

テンプレート実引数推論のルールを正確に示すとかなり難しいですが、かなり多くのパターンの推論が行えます[6]。たとえば、関数の仮引数が T*const T& であっても、きちんと T の部分を推論してくれます。

#include <iostream>

template <typename T>
void f(T* a, const T& b)
{
    *a = b;
}

int main()
{
    int a {0};
    int b {100};
    f(&a, b);
    std::cout << a << "\n";
}

実行結果:

100

先に挙げた、std::vector<T> から mylib::RingBuffer<T> にコピーする関数テンプレートでも、T の部分をテンプレート実引数推論させられます。

#include <vector>
#include "ring_buffer.h"

template <typename T>
mylib::RingBuffer<T>& copy_vector_to_ringbuffer(const std::vector<T>& vec, mylib::RingBuffer<T>& ringbuf)
{
    for (const auto& v : vec) {
        ringbuf.push_back(v);
    }
    return ringbuf;
}

int main()
{
    std::vector<int> vec {10, 20, 30};
    mylib::RingBuffer<int> rb(8);

    copy_vector_to_ringbuffer(vec, rb);  // テンプレート実引数推論により、T は int
}

実行結果:

テンプレート実引数推論の結果が食い違う場合はコンパイルエラーになります。

#include <iostream>

template <typename T>
void f(T* a, const T& b)
{
    *a = b;
}

int main()
{
    int a {0};
    double b {100.0};
    f(&a, b);  // エラー。&a からは int、b からは double が推論され食い違う
    std::cout << a << "\n";
}

テンプレート実引数推論は、テンプレート仮引数が関数の仮引数として使われていない場合や、そもそも関数に仮引数がない場合などには使えません。

#include <iostream>

template <typename T>
T get_zero()
{
    return 0;
}

int main()
{
    auto r1 = get_zero();       // エラー。判断に使える実引数がない
    auto r2 = get_zero<int>();  // OK
    auto r3 = get_zero<int*>(); // OK
}

デフォルトテンプレート実引数 🔗

クラステンプレートと同じく(「クラステンプレート」のページを参照)、関数テンプレートのテンプレート仮引数にデフォルト値(デフォルトテンプレート実引数 (default template augument))を指定できます。

デフォルトの型は、テンプレート仮引数を記述するときに = と型名を続けて書くことで指定できます。

template <テンプレート仮引数 =>

テンプレート仮引数にデフォルトテンプレート実引数が指定されていても、テンプレート実引数を明示的に与えた場合、またはテンプレート実引数推論によって決定できた場合には、デフォルトテンプレート実引数の指定は無視されます。こうした方法でテンプレート実引数が決定されなかったときに、デフォルトテンプレート実引数の指定が使われます。[7]

#include <iostream>

template <typename T1, typename T2 = double>
void f1(T1 a, T2 b)
{
    std::cout << a * b << "\n";
}

template <typename T1, typename T2 = double>
T2 f2(T1 a, T1 b)
{
    auto r = static_cast<T2>(a * b);
    std::cout << r << "\n";
    return r;
}

int main()
{
    f1<double, int>(12.3, 15);  // 明示的な指定で T1 は double、T2 は int
    f1(10, 15);                 // テンプレート実引数推論で T1、T2 はともに int
    f1<double>(12.3, 15);       // 明示的な指定で T1 は double。T2 はテンプレート実引数推論により int

    f2<int, int>(10, 15);       // 明示的な指定で T1、T2 は int
    f2(10, 15);                 // 明示的な指定がなく、テンプレート実引数推論もできないため、T2 はデフォルトの double
    f2<double>(12.3, 15);       // 明示的な指定で T1 は double。T2 はデフォルトの double
}

実行結果:

184.5
150
184.5
150
150
184.5

f1 のほうの例をみるとわかるように、明示的な指定か、テンプレート実引数推論によって、T2 の型は決まってしまうため、T2 に与えているデフォルトテンプレート実引数が機能するケースはありません。デフォルトテンプレート実引数が意味をなすのは、そのテンプレート仮引数が仮引数のところに登場しない場合に限られるということになります。たとえば、そのテンプレート仮引数が戻り値の型にしか使われていない場合や、本体のコードの中でしか登場しない場合には、使い道がうまれます。


先に定義されたテンプレート仮引数を、後続のテンプレート仮引数のデフォルトの「型」に使うこともできます。

template <typename T1, typename T2 = T1>

クラステンプレートの場合、デフォルトテンプレート実引数があるテンプレート仮引数のうしろに、デフォルトテンプレート実引数がないテンプレート仮引数を続けられない制約がありますが(「クラステンプレート」のページを参照)、関数テンプレートにはこの制約はありません[8]

// T2 にデフォルトテンプレート実引数があるが、T3 になくても構わない
template <typename T1, typename T2 = double, typename T3>

ただし、実際にこの関数テンプレートを使用するときには、すべてのテンプレート仮引数に渡されるテンプレート実引数が決定できなければなりません。

#include <iostream>

template <typename T1, typename T2 = double, typename T3>
void f1(T1 a, T2 b, T3 c)
{
    std::cout << a << ", " << b << ", " << c << "\n";
}

template <typename T1, typename T2 = double, typename T3>
T3 f2(T1 a, T2 b)
{
    auto r = static_cast<T3>(a * b);
    std::cout << r << "\n";
    return r;
}


int main()
{
    f1(10, 23.45, "abc");   // OK. テンプレート実引数推論で T3 のテンプレート実引数が決定できる

    //f2(10, 23.45);        // コンパイルエラー. T3 のテンプレート実引数が不明
    f2<int, double, double>(10, 23.45);  // OK. T3 のテンプレート実引数を明示的に指定
}

実行結果:

10, 23.45, abc
234.5

引数の型から戻り値の型を決める 🔗

add関数テンプレートを改造することを考えてみます。現状の add関数テンプレートは、2つの仮引数の型がともに T になっているため、auto result = add(10, 5.5); のように型が異なる実引数を渡すことができません。しかし、加算した結果を返すという関数の意味からいえば、これは余計な制約かもしれません。

そこで、仮引数それぞれの型を別々のテンプレート仮引数であらわすようにします。

template <typename T1, typename T2>
??? add(T1 a, T2 b)
{
    return a + b;
}

戻り値の型はどうなるべきでしょうか。T1 と T2 という、実際にはなにが指定されるか分からない未知の型どうしの加算結果なので、関数テンプレートの実装側としては決めかねます。もっとも単純な方法は auto を指定して、戻り値の型推論に任せることです(「関数から値を返す」のページを参照)。

#include <iostream>

template <typename T1, typename T2>
auto add(T1 a, T2 b)
{
    return a + b;
}

int main()
{
    auto r1 = add(10, 20);    // T1 == int、T2 == int。 int + int の結果は int
    auto r2 = add(5.5, 3.0);  // T1 == double、T2 == double。 double + double の結果は double
    auto r3 = add(10, 3.5);   // T1 == int、T2 == double。 int + double の結果は double
    std::cout << r1 << "\n";
    std::cout << r2 << "\n";
    std::cout << r3 << "\n";
}

実行結果:

30
8.5
13.5

戻り値の型を呼び出し側で決定させたい場合は、戻り値の型をあらわすテンプレート仮引数を追加する方法が使えます。次の例では、static_cast で変換できる型であれば、自由に戻り値の型を選べます。

#include <iostream>

template <typename R, typename T1, typename T2>
R add(T1 a, T2 b)
{
    return static_cast<R>(a + b);
}

int main()
{
    auto r1 = add<int>(10, 20);
    auto r2 = add<long long>(5.5, 3.0);  // long long で受け取る
    auto r3 = add<double>(10, 3.5);
    std::cout << r1 << "\n";
    std::cout << r2 << "\n";
    std::cout << r3 << "\n";
}

実行結果:

30
8
13.5

2つの仮引数の型はテンプレート実引数推論に任せ、戻り値の型は明示的な指定を行います。これを実現するためには、明示的に指定するテンプレート仮引数を先頭に持ってくる工夫が必要です。template <typename T1, typename T2, typename R> のような順番で宣言してしまうと、3つとも明示的に指定しなければならなくなります。

メンバテンプレート 🔗

テンプレートをクラスやクラステンプレートの定義の内側で宣言した場合、メンバテンプレート (member template) と呼ばれます。

メンバテンプレートは総称です。そのテンプレートがメンバ関数を関数テンプレートにしたものであればメンバ関数テンプレート (member function template) と呼ばれます。そのほかにも、エイリアステンプレート(「クラステンプレート」のページを参照)、入れ子クラス(「静的メンバ」のページを参照)をテンプレートにすることなども含まれます。

あとで取り上げる変数テンプレートも、クラス定義内にあればメンバテンプレートの一種になります。

【上級】ローカルクラスはメンバテンプレートを持つことができません。また、デストラクタをメンバテンプレートにはできません。[9]

メンバテンプレートの宣言をクラステンプレート定義の内側で行った場合、クラステンプレート側のテンプレート仮引数と、メンバテンプレート側のテンプレート仮引数がそれぞれに存在することになります。

メンバ関数テンプレート 🔗

メンバ関数をテンプレートにしたものはメンバ関数テンプレートと呼ばれます。コンストラクタをテンプレートにすることも可能です(コンストラクタについては改めて取り上げます)。

【上級】デストラクタをメンバテンプレートにはできません[9]。また、メンバ関数テンプレートを仮想関数にはできません。[10]

クラス定義の内側で宣言する場合は以下のようになります。

class クラス名 {
    // 宣言
    template <テンプレート仮引数の宣言の並び>
    戻り値の型 メンバ関数テンプレートの名前(仮引数の並び);
};

// 定義
template <テンプレート仮引数の宣言の並び>
戻り値の型 クラス名::メンバ関数テンプレートの名前(仮引数の並び)
{
    本体のコード
}

クラス定義の内側に直接定義を置いても構いません。

class クラス名 {
    // 定義
    template <テンプレート仮引数の宣言の並び>
    戻り値の型 メンバ関数テンプレートの名前(仮引数の並び)
    {
        本体のコード
    }
};

以下は使用例です。クラステンプレートではないので、変数を宣言するときにテンプレート実引数の指定は現れません。

#include <iostream>

class C {
public:
    explicit C(double v) : m_value {v}
    {}

    template <typename T>
    T get() const;

private:
    double  m_value;
};

template <typename T>
T C::get() const
{
    return static_cast<T>(m_value);
}

int main()
{
    C c {12.345};

    std::cout << c.get<double>() << "\n";
    std::cout << c.get<int>() << "\n";
}

実行結果:

12.345
12

クラステンプレート定義の内側で宣言する場合は以下のようになります。クラステンプレート定義の外側に、メンバ関数テンプレートの定義を書く場合、クラステンプレート側のテンプレート仮引数と、メンバ関数テンプレート側のテンプレート仮引数をそれぞれ記述しなければなりません。

template <クラステンプレート側のテンプレート仮引数の宣言の並び>
class クラステンプレート名 {
    // 宣言
    template <メンバ関数テンプレート側のテンプレート仮引数の宣言の並び>
    戻り値の型 メンバ関数テンプレートの名前(仮引数の並び);
};

// 定義
template <クラステンプレート側のテンプレート仮引数の宣言の並び>
template <メンバ関数テンプレート側のテンプレート仮引数の宣言の並び>
戻り値の型 クラステンプレート名<クラステンプレート側のテンプレート仮引数の名前の並び>::メンバ関数テンプレートの名前(仮引数の並び)
{
    本体のコード
}

クラステンプレート定義の内側に直接定義を置いても構いません。

template <クラステンプレート側のテンプレート仮引数の宣言の並び>
class クラステンプレート名 {
    // 定義
    template <メンバ関数テンプレート側のテンプレート仮引数の宣言の並び>
    戻り値の型 メンバ関数テンプレートの名前(仮引数の並び)
    {
        本体のコード
    }
};

以下は使用例です。

#include <iostream>

template <typename T>
class C {
public:
    explicit C(T v) : m_value {v}
    {}

    template <typename U>
    U get() const;

private:
    T  m_value;
};

template <typename T>
template <typename U>
U C<T>::get() const
{
    return static_cast<U>(m_value);
}

int main()
{
    C<double> c {12.345};

    std::cout << c.get<double>() << "\n";
    std::cout << c.get<int>() << "\n";
}

実行結果:

12.345
12

依存名と templateキーワード 🔗

クラステンプレート」のページで解説したように、テンプレート仮引数が絡んでいる名前(依存名)は、原則として型の名前であるとは判断されないので、typenameキーワードを補わなければならいことがありました。

これとよく似た問題がメンバ関数テンプレートを使うときに起こることがあります。

#include <iostream>

class C {
public:
    explicit C(double v) : m_value {v}
    {}

    template <typename T>
    T get() const;

private:
    double  m_value;
};

template <typename T>
T C::get() const
{
    return static_cast<T>(m_value);
}



template <typename T>
void print_value(const T& t)
{
    std::cout << t.get<int>() << "\n";  // コンパイルエラー (Visual Studio では発生しない)
}


int main()
{
    C c {12.345};

    print_value<>(c);
}

Visual Studio 2015 で試すとエラーになりませんが、gcc 6.4.0 などでは、t.get<int>() の部分でコンパイルエラーになります。

問題は get<int> が依存名とみなされることにあります。get は関数テンプレートなので、テンプレート仮引数 T に依存しています。この場合、get がテンプレートの名前であるとは判断されず、そのため続く < がテンプレート実引数の指定ではなく、比較演算子の < と判断します。結果、getint を比較しようとする文法エラーになります。

【上級】正確なルールは規格を参照してください[11]

そこで、テンプレート名の手前に templateキーワードを置くことによって、直後の名前がテンプレートの名前であることを明確にします。templateキーワードを置く位置は非常に不自然ですが、get が問題なので、その直前に挟み込んで t. template get<int>() のようにします。

#include <iostream>

class C {
public:
    explicit C(double v) : m_value {v}
    {}

    template <typename T>
    T get() const;

private:
    double  m_value;
};

template <typename T>
T C::get() const
{
    return static_cast<T>(m_value);
}



template <typename T>
void print_value(const T& t)
{
    std::cout << t. template get<int>() << "\n";  // OK
}


int main()
{
    C c {12.345};

    print_value<>(c);
}

実行結果:

12

テンプレートコンストラクタ 🔗

コンストラクタもテンプレートにでき、テンプレートコンストラクタ (template constructor) やコンストラクタテンプレート (constructor template) などと呼ばれます。

以前「コンストラクタ」のページで、コンストラクタを、実引数の型をクラス型へ変換する仕組みと捉えられるという話をしました。このようなコンストラクタを特に、変換コンストラクタと呼びました。

class Money {
public:
    // int から Money に変換する変換コンストラクタ
    explicit Money(int amount) : m_amount{amount}
    {}

private:
    int  m_amount;
};

これと同様に、テンプレートコンストラクタを型変換の仕組みであると捉えることができます。たとえば、Moneyクラスをクラステンプレートにして、内部的な型を指定できるようにしたとします。すると、Money<int> から Money<long> は異なる型ということになります(「クラステンプレート」のページを参照)。そのため、Money<int> から Money<long> への変換はできません。

#include <iostream>

template <typename T>
class Money {
public:
    explicit Money(T amount) : m_amount{amount}
    {}

    // ほかの Money<T> からコピーを作るコンストラクタ
    explicit Money(const Money<T>& other) : m_amount{other.m_amount}
    {}

    inline T get_amount() const
    {
        return m_amount;
    }

private:
    T  m_amount;
};

int main()
{
    Money<int> m1 {1000};
    Money<long> m2 {m1};  // エラー。Money<int> は Money<long> ではない

    std::cout << m1.get_amount() << "\n";
    std::cout << m2.get_amount() << "\n";
}

そこで、コピーを作るためのコンストラクタを、テンプレートによる変換コンストラクタ、すなわちテンプレート変換コンストラクタ (template conversion constructor) に変更します。

#include <iostream>

template <typename T>
class Money {
public:
    explicit Money(T amount) : m_amount{amount}
    {}

    // Money<U> から Money<T> に変換するテンプレート変換コンストラクタ
    template <typename U>
    explicit Money(const Money<U>& other) : 
        m_amount{static_cast<T>(other.get_amount())}
    {}

    inline T get_amount() const
    {
        return m_amount;
    }

private:
    T  m_amount;
};

int main()
{
    Money<int> m1 {1000};
    Money<long> m2 {m1};   // OK. Money<int> から Money<long> に変換
    Money<short> m3 {m1};  // OK. Money<int> から Money<short> に変換

    std::cout << m1.get_amount() << "\n";
    std::cout << m2.get_amount() << "\n";
    std::cout << m3.get_amount() << "\n";
}

実行結果:

1000
1000
1000

U から T へ変換する実装が可能なのであれば、こうしてテンプレート変換コンストラクタを用意することで、テンプレート実引数が異なるクラステンプレート間の変換が実現できます。このサンプルコードでは、テンプレートコンストラクタの仮引数 other から get_amountメンバ関数を呼び出し、返された値を T にキャストしたものを使ってデータメンバを初期化することで実現しています。

ここで、get_amountメンバ関数を呼んでいる点に注目します。次のように、仮引数 other のメンバ変数 m_amount にアクセスするように書こうとするとコンパイルエラーになります。

    // Money<U> から Money<T> に変換するテンプレート変換コンストラクタ
    template <typename U>
    explicit Money(const Money<U>& other) : 
        m_amount{static_cast<T>(other.m_amount)}  // エラー。m_amount にアクセスできない
    {}

なぜなら、Money<T>Money<U> は異なる型であるから、相手の private なメンバにはアクセスできないためです。これは同一のクラスであれば可能なことです。

    explicit Money(const Money<T>& other) : 
        m_amount{other.m_amount}  // OK. m_amount にアクセスできる
    {}

このため、前のサンプルコードでは public なメンバを使って実装したわけですが、普通、すべてのメンバ変数を適切に初期化するのに必要な情報を public に公開するわけにもいかないはずです。そこで、選ばれた相手にだけ public でないメンバにアクセスすることを許す、フレンド (friend) と呼ばれる機能を使います。

フレンドクラス 🔗

friend指定子 (friend specifier) を使って、クラス定義の内側にクラスの宣言を行うと、そのクラスから public でないメンバにアクセスすることを許可できます。許可を与えられた側のクラスのことを、フレンドクラス (friend class) と呼びます。また、「A は B のフレンド(クラス)である」のような表現をします。

class クラス名 {
    friend class フレンドクラス名;
    friend struct フレンドクラス名;
};

クラステンプレートをフレンドクラスにしたいときは2つの選択肢があります。1つは、テンプレート実引数を指定することで、それと一致する特殊化の場合だけをフレンドクラスとする方法です。

class クラス名 {
    friend class フレンドクラス名<テンプレート実引数の並び>;
    friend struct フレンドクラス名<テンプレート実引数の並び>;
};

もう1つは、フレンドの記述自体をテンプレートにすることで、すべての特殊化をフレンドクラスとする方法です。

class クラス名 {
    template <テンプレート仮引数の並び>
    friend class フレンドクラス名;

    template <テンプレート仮引数の並び>
    friend struct フレンドクラス名;
};

Moneyクラステンプレートの例もこの方法を使って、Money クラステンプレートのすべての特殊化をフレンドクラスとして指定すれば、Money<T> から Money<U> の public でないメンバをアクセスできるようになります。

#include <iostream>

template <typename T>
class Money {

    // Money のすべての特殊化をフレンドクラスとする
    template <typename T>
    friend class Money;

    // 一部の特殊化だけをフレンドクラスとする場合は、上記の代わりに次のように書く。
    // friend class Money<long>;
    // friend class Money<short>;

public:
    explicit Money(T amount) : m_amount{amount}
    {}

    // Money<U> から Money<T> に変換するテンプレート変換コンストラクタ
    template <typename U>
    explicit Money(const Money<U>& other) : 
        m_amount{static_cast<T>(other.m_amount)} // OK. m_amount にアクセスできる
    {}

    inline T get_amount() const
    {
        return m_amount;
    }

private:
    T  m_amount;
};

int main()
{
    Money<int> m1 {1000};
    Money<long> m2 {m1};
    Money<short> m3 {m1};

    std::cout << m1.get_amount() << "\n";
    std::cout << m2.get_amount() << "\n";
    std::cout << m3.get_amount() << "\n";
}

実行結果:

1000
1000
1000

フレンド機能は、このような用途に限らずに使用できるものですが、指定する相手は慎重に検討してください。Money<T> に対する Money<U> のように、ごく近しい関係性がある相手だからこそ問題になりませんが、public でないところにアクセスし放題になる機能なので、気軽に使っていいものではありません。

フレンド関数 🔗

フレンドにできる相手として関数を指定することも可能です。許可を与えられた関数のことをフレンド関数 (friend function) と呼びます。

class クラス名 {
    friend フレンド関数の宣言;
};

関数テンプレートをフレンド関数にする場合は、特定の特殊化だけを対象にする方法と、すべての特殊化を対象にする方法があります。

class クラス名 {
    friend class フレンド関数の宣言<テンプレート実引数の並び>;

    template <テンプレート仮引数の並び>
    friend フレンド関数の宣言;
};

フレンドクラスのところのサンプルプログラムを変形して、Money<U> から Money<T> への変換を関数テンプレートで行うようにすると次のようになります。

#include <iostream>

template <typename T>
class Money {

    template <typename T, typename U>
    friend Money<T> convert(const Money<U>& other);

public:
    explicit Money(T amount) : m_amount{amount}
    {}

    inline T get_amount() const
    {
        return m_amount;
    }

private:
    T  m_amount;
};

template <typename T, typename U>
Money<T> money_convert(const Money<U>& other)
{
    // money_convert関数テンプレートがフレンドでなければ、other.m_amount にアクセスできない
    return Money<T>{static_cast<T>(other.m_amount)};
}

int main()
{
    Money<int> m1 {1000};
    Money<long> m2 {money_convert<long>(m1)};
    Money<short> m3 {money_convert<short>(m1)};

    std::cout << m1.get_amount() << "\n";
    std::cout << m2.get_amount() << "\n";
    std::cout << m3.get_amount() << "\n";
}

実行結果:

1000
1000
1000

ジェネリックラムダ 🔗

再帰呼び出しとスタック」のページで登場したジェネリックラムダは、仮引数の型を auto にすることで、ラムダ式に任意の型の値を渡せるというものでした。特定の型への依存をなくしたラムダ式ということなので、これはテンプレートと同じ考え方であり、実際その仕組みの中にテンプレートが登場します。

通常のラムダ式では、次のようなクロージャ型が生成されます(「関数ポインタとラムダ式」のページを参照)。

// このようなかたちのラムダ式から・・
// [](int a, double b) -> 戻り値の型 {本体};

// 以下のようなクラスが生成される
class ???? {
public:
    戻り値の型 operator()(int a, double b) const
    {
        本体
    }
};

ジェネリックラムダになると、次のように変化します。

// このようなかたちのジェネリックラムダから・・
// [](auto a, auto b) -> 戻り値の型 {本体};

// 以下のようなクラスが生成される
class ???? {
public:
    template <typename T1, typename T2>
    戻り値の型 operator()(T1 a, T2 b) const
    {
        本体
    }
};

このように、operator() がメンバ関数テンプレートになることで、任意の型の値を渡すことが可能になるという仕組みです。テンプレート仮引数は、ジェネリックラムダの仮引数それぞれに対して異なるものが割り当てられます(この例では a に対して T1b に対して T2)。

ラムダ式のすべての仮引数を auto にしなければならないわけではなく、一部だけでも構いません。operator() の仮引数は、auto を指定した引数についてだけテンプレート仮引数になります。

なお、auto*const auto& のように修飾することも可能で、生成される operator() の仮引数に反映されます。

【上級】[](auto... args){} のようにパラメータパックを使うことも可能です。

一方で、[](std::vector<auto>& vec){} のような使い方はできません。

【C++20】ジェネリックラムダに、関数テンプレートに似た記法が追加され、[]<typename T>(std::vector<T>& vec){} のような記述が可能になりました[12]

次のコードは使用例です。

#include <iostream>

int main()
{
    auto add = [](auto a, auto b) { return a + b; };

    auto r1 = add(10, 20);
    auto r2 = add(5.5, 3.0);
    auto r3 = add(10, 3.5);
    std::cout << r1 << "\n";
    std::cout << r2 << "\n";
    std::cout << r3 << "\n";
}

実行結果:

30
8.5
13.5

ジェネリックラムダでもキャプチャの指示は可能です(「関数ポインタとラムダ式」のページを参照)。

通常のラムダ式と同様、キャプチャを用いていなければ、ジェネリックラムダから関数ポインタに変換することは可能です(「関数ポインタとラムダ式」のページを参照)。関数ポインタの仮引数や戻り値の型には auto を指定できないので、ジェネリックラムダの auto を使っている部分が具体的な型に置き換えられたあとの型に合わせなければなりません。

#include <iostream>

int main()
{
    auto add = [](auto a, auto b) { return a + b; };

    int (*op)(int, int) = add;     // OK。仮引数a、b に渡す実引数が int の場合に呼び出せる 
//  int (*op)(int, double) = add;  // エラー。戻り値の型は int + double から型推論するので、double でなければならない

    auto r = op(10, 20);  // OK
    std::cout << r << "\n";
}

実行結果:

30

std::function(「関数ポインタとラムダ式」のページを参照)に渡す場合も同様で、置き換え後の型と一致していなければなりません。

#include <functional>
#include <iostream>

int main()
{
    auto add = [](auto a, auto b) { return a + b; };

    std::function<int (int, int)> f = add;  // OK

    auto r = f(10, 20);
    std::cout << r << "\n";
}

実行結果:

30

変数テンプレート 🔗

C++14 になって、変数テンプレート (variable template) という新たなテンプレートが追加されました。名前のとおり、変数をテンプレートにできる機能です。

template <テンプレート仮引数>
型 変数名 初期化子;

「テンプレート仮引数」を、「型」や「初期化子」のところで使うことができます。

ほかのテンプレートでも同様ですが、名前空間スコープ(グローバルスコープも含む)、クラススコープの中でのみ定義できます[13]。したがって、ローカル変数の変数テンプレートは作れません。また、クラススコープ内に変数テンプレートを作る場合(つまりデータメンバをテンプレートにする場合)は、静的データメンバでなければなりません[14]

変数テンプレートの名前のうしろに <テンプレート実引数> を続けることでインスタンス化できます。もちろん生成された実体(特殊化)は変数です。

次のコードは使用例です。

#include <iostream>

template <typename T>
T value {static_cast<T>(10)};

int main()
{
    std::cout << value<int> << "\n";
    value<int> = 100;
    std::cout << value<int> << "\n";

    std::cout << value<double> << "\n";
    value<double> = 12.5;
    std::cout << value<double> << "\n";
}

実行結果:

10
100
10
12.5

Visual Studio 2015 の変数テンプレートの実装は不完全なようで、正常な記述がコンパイルエラーになることがあり、このサンプルプログラムもエラーになります。問題は以下の2つです。

  1. 変数テンプレートの初期化子に {} の記法が使えない
  2. 変数テンプレートの初期化時に static_cast が使えない

そのため、Visual Studio 2015 では、= と、関数形式キャスト(「コンストラクタ」のページを参照)かC言語スタイルのキャストに書き換えざるをえないようです。

template <typename T>
T value = T(10);

新C++編ではこれ以降、Visual Studio 2015 でもコンパイルできる記述で統一します。なお、この不具合は Visual Studio 2017 では修正されています。


テンプレートにできる変数には constexpr変数も含まれており、むしろこの用途で用いることが多いです。

#include <iostream>

template <typename T>
constexpr T pi = T(3.14159265358979323846);

int main()
{
    std::cout << pi<double> << "\n";
    std::cout << pi<int> << "\n";
}

実行結果:

3.14159
3

この使用方法は関数テンプレートでも実現できますが、constexpr変数を使うとコンパイル時に評価できるため、効率が良くなることが期待できます。また、() を書かなくてよくなります。

#include <iostream>

template <typename T>
T pi()
{
    return static_cast<T>(3.14159265358979323846);
}

int main()
{
    std::cout << pi<double>() << "\n";
    std::cout << pi<int>() << "\n";
}

実行結果:

3.14159
3

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (基本★)

引数に渡した2つの値のうち、大きい方を返す関数テンプレートを作成してください。つまり、標準ライブラリの std::max関数(「UTF-8」のページを参照)と同じものを作ることになります。

解答・解説

問題2 (基本★★)

任意の要素型の std::vector を渡すと、含まれている要素の平均値を返す関数テンプレートを作成してください。

解答・解説

問題3 (応用★★)

RingBufferクラステンプレートに、要素型が異なる RingBuffer から型変換するテンプレート変換コンストラクタを追加してください。

現状の RingBufferクラステンプレートの実装は、「クラステンプレート」のページにあります。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る