関数テンプレート | Programming Place Plus Modern C++編【言語解説】 第11章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


関数テンプレート 🔗

第10章で説明した関数オーバーロードを使えば、異なる型を持った同名の関数を作れますが、関数の作成者が想定した範囲内でしか対応できません。しかも、型ごとに1つ1つ関数を定義しなければなりません。何より、関数オーバーロードでは、将来追加されるかもしれない未知の型にまでは対応できません。

そこで登場するのが、関数テンプレートという機能です。第8章でクラステンプレートを紹介しましたが、考え方は同様で、これの関数版です。関数テンプレートを使うと、1つの関数の実装を複数の型で使いまわすことが可能です。これは、未知の型であっても対応できる適用力を持っています。

関数テンプレートを使うにはまず、通常の関数のように宣言と定義を記述します。このとき、先頭に template というキーワードを置くことによって、それが通常の関数ではなく、関数テンプレートであることを表します。

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

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

typenameキーワードは、変わりに classキーワードを使っても構いません。まったく同じ意味になります。

< と > の間に記述される内容は、テンプレート仮引数(テンプレートパラメータ)と呼ばれる指定です。「<typename T>」と書いた場合、T という名前のテンプレート仮引数を定義しています。T は慣例的に使われている名前です。もちろん、分かりやすく命名することが理想的です。

テンプレート仮引数は複数あっても構いませんが、最低でも1つは必要です。テンプレート仮引数を複数個使う例は、後で取り上げます

テンプレート仮引数名の手前に typename や class を置くことは、そのテンプレート仮引数が「型」であることを意味しています。型でないテンプレート仮引数もあり得ます(後述)。

関数テンプレートの定義の中には、通常の関数を実装するのと同じように本体のコードを記述します。本体のコードおよび、仮引数や戻り値のところでは、テンプレート仮引数を使用できます。通常の関数であれば、「int」とか「char」などと記述しますが、その代わりに「T」のような記述が可能になるということです。

関数テンプレートで使われているテンプレート仮引数は、その関数テンプレートを使用するときに与えるテンプレート実引数によって置き換えられます。

試しに、前章の関数オーバーロードで使った write関数の例を、関数テンプレートを使って書き換えてみます。

#include <iostream>
#include <string>

template <typename T>
void write(T a);

int main()
{
    write<int>(100);
    write<double>(3.5);
    write<char>('x');
    write<std::string>("xyz");
}

template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}

実行結果:

100
3.5
x
xyz

いつものように、宣言と定義はまとめてしまっても構いません。

通常の関数を使用するときに、関数呼び出しを行うのと同様に、関数テンプレートを使用するときにも、関数呼び出しの構文を用います。このとき、テンプレート実引数の指定を行えます。

関数テンプレート名<テンプレート実引数の並び>(実引数の並び)

テンプレート仮引数が複数あるのなら、その順番どおりに「,」で区切って書き並べます。

指定したテンプレート実引数が、テンプレート仮引数のところに当てはめられます。たとえば、1つ目の呼び出しでは「int」が指定されているので、write関数テンプレートのテンプレート仮引数 T は「int」で置き換えられます。同様に2つ目の呼び出しでは、T を「double」に置き換えます。

コンパイラは関数テンプレートの実際の使われ方をみて、テンプレート仮引数をテンプレート実引数で置き換えたコードを生成します。たとえば、次のようになります。

void write(int a)
{
    std::cout << a << std::endl;
}

void write(double a)
{
    std::cout << a << std::endl;
}

この置き換え後のコードは、通常の関数と同じ構文ルールに従っていることが分かります。つまり、関数テンプレートから関数が生み出されているといえます。これは、関数テンプレートの実体化と呼ばれています。また、実体化して生み出された関数を、関数テンプレートの特殊化といいます。

特殊化という用語の意味はかなり分かりづらいと思います。「化」が付いているため、これから起こる何らかの変化のことを言っていそうですが、そうではなく、変化(=実体化)を終えた結果、できあがったもののことを言っています。

実体化は、テンプレート実引数が異なる呼び出しを見つけるたびに行われるため、使われ方のパターンが多いと、それだけプログラムサイズは大きくなります。

複数個のテンプレート仮引数 🔗

テンプレート仮引数は複数個あっても構いません。

#include <iostream>

template <typename T1, typename T2>
void write_max(T1 a, T2 b)
{
    std::cout << ((a >= b) ? (a) : (b)) << std::endl;
}

int main()
{
    write_max<int, int>(10, 11);
    write_max<double, int>(10.5, 10);
    write_max<int, double>(10, 10.5);
}

実行結果:

11
10.5
10.5

write_max関数は、2つの実引数のうち、大きい方を選んで標準出力へ出力します。このとき、2つの実引数の型が異なることを許すため、2つのテンプレート仮引数を利用しています。もし、1つのテンプレート仮引数だけで実現しようとすると、2つのテンプレート実引数が同じ型でなければならなくなります。

#include <iostream>

template <typename T>
void write_max(T a, T b)
{
    std::cout << ((a >= b) ? (a) : (b)) << std::endl;
}

int main()
{
    write_max<int>(10, 11);
    write_max<double>(10.5, 10);
    write_max<double>(10, 10.5);
    write_max<int>(10, 10.5);
}

実行結果:

11
10.5
10.5
10

実引数の値を、テンプレート実引数の型に暗黙的に変換できる限りは、コンパイルは通りますが、情報を失う恐れはあります。4つ目の呼び出しでは、10.5 という double型の値を、int型に変換しています。

テンプレート実引数の推定 🔗

ここまでのサンプルプログラムでは、関数テンプレートの呼び出しの際に、テンプレート実引数を明示的に指定していますが、テンプレート実引数の型を、コンパイラに自動判断させることができます。この機能は、テンプレート実引数の推定と呼ばれます。

テンプレート実引数の推定は、関数テンプレートを呼び出す際に渡す実引数の型を使って行われます。たとえば、write関数テンプレートの例で考えてみます。write関数テンプレートは、次のように宣言されています。

template <typename T>
void write(T a);

テンプレート実引数の推定を使うには、この関数テンプレートを使用するときに、次のように書きます。

write(100);

テンプレート仮引数 T が、仮引数a の型として使われています。そのため、仮引数a の型が決まれば、それがテンプレート仮引数 T の型であると考えられます。そして、仮引数a の型は、実引数の型から判断できます。

実引数の「100」は int型なので、仮引数a も int型、テンプレート仮引数 T も int型であると判断できるという流れです。いわば、反対側(使用者の側)から確定させていくような流れです。

テンプレート実引数の推定は積極的に使ってよい機能です。明示的にテンプレート実引数を指定する方法では、暗黙の型変換が入るせいで、指定間違いを検出できないことがあります。

この章の最初のサンプルプログラムを書き換えてみます。

#include <iostream>

template <typename T>
void write(T a);

int main()
{
    write(100);    // 100 は int なので、T は int
    write(3.5);    // 3.5 は double なので、T は double
    write('x');    // 'x' は char なので、T は char
    write("xyz");  // "xyz" は const char[] なので、T は const char*
}

template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}

実行結果:

100
3.5
x
xyz

4つ目の呼び出しだけは、最初のサンプルプログラムとは結果が変わっています。文字列リテラルは、const char[] であって、std::string ではありません。また、配列のまま渡せないので、const char* であると扱われます。もし、std::string として扱わせたいのであれば、テンプレート実引数を明示的に指定します。

テンプレート仮引数が複数個ある場合、末尾側だけをテンプレート実引数の推定に任せて、手前側は明示的に指定するという方法を取ることもできます。次のサンプルプログラムで、3つの関数テンプレートの呼び出しは、いずれも同じ意味です。

#include <iostream>

template <typename T1, typename T2>
void write_max(T1 a, T2 b)
{
    std::cout << ((a >= b) ? (a) : (b)) << std::endl;
}

int main()
{
    write_max(10, 10.5);
    write_max<int>(10, 10.5);
    write_max<int, double>(10, 10.5);
}

実行結果:

10.5
10.5
10.5

ところで、戻り値の型にテンプレート仮引数を使う場合は、関数の実引数からは推測ができません。あまり考えずに実装すると、次のように明示的にテンプレート実引数を指定しなければならなくなり、不便です。

#include <iostream>

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

int main()
{
    // T1、T2、RET の型をそれぞれ明示的に指定せざるを得ない
    double result = add<int, double, double>(5, 5.2);
    std::cout << result << std::endl;
}

実行結果:

10.2

こういうケースでは、戻り値の型に使うテンプレート仮引数を1つ目に定義します。テンプレート実引数の推定に任せられない部分を手前側に持ってくれば、そこだけ明示的に指定して、残りは推定させることができるという考え方です。

#include <iostream>

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

int main()
{
    // RET の型だけを明示的に指定し、T1、T2 の型は実引数から推定させる
    std::cout << add<double>(5, 5.2) << std::endl;
}

実行結果:

10.2

テンプレート仮引数と関数の仮引数 🔗

関数テンプレートが、関数の仮引数の部分でテンプレート仮引数を使うとき、単純に「f(T a)」のように使う以外に、ほかの要素を組み合わせた使い方ができます。

次の例では、「T」を「T*」として使っています。

#include <iostream>

template <typename T>
void write(T* p)
{
    std::cout << *p << std::endl;
}

int main()
{
    int v = 100;

    write<int>(&v);
    write(&v);
}

実行結果:

100
100

テンプレート実引数を明示的に指定している方に注目すると、「<int>」となっています。ここが「<int*>」でないことは重要です。「*」は仮引数 p の方に付いていますから、テンプレート仮引数 T に当てはめるべき型は「int」であって、「int*」ではないのです。

テンプレート実引数の推定に任せている方も同じ結果になります。実引数は「&v」なので「int*」です。仮引数 p の型が「T*」なので、「T」に当てはめるべきものは「int」であることを推定してくれます。

write関数テンプレートの仮引数 p を constポインタに変えても、やはり理屈は変わらず、同じ結果になります。

#include <iostream>

template <typename T>
void write(const T* p)
{
    std::cout << *p << std::endl;
}

int main()
{
    int v = 100;

    write<int>(&v);  // OK. T は int、p は const int*
    write(&v);       // OK. T は int、p は const int*
}

実行結果:

100
100

仮引数 p の方を非constポインタに戻して、ローカル変数 v を const にすると、コンパイルエラーになります。

#include <iostream>

template <typename T>
void write(T* p)
{
    std::cout << *p << std::endl;
}

int main()
{
    const int v = 100;

    write<int>(&v);  // コンパイルエラー
                     // T を int とすると、仮引数 p は int* であり、constポインタを渡せない
    write(&v);       // OK。T は const int、p は const int*
}

これは明示的に指定したテンプレート実引数に問題があります。「T」を「int」としてしまうと、仮引数 p の型は「int*」になりますから、「const int*」である「&v」を渡せません。

一方、テンプレート実引数の推定に任せている方は、「&v」が「const int*」であることを認識できるため、「T」に当てはめるべきものが「const int」であることを判断可能です。「T」が「const int」に置き換えられると、仮引数 p の型は「const int*」になりますから、「&v」を渡せます。

ところで、仮引数が「T*」のようにポインタ型になっていなければ、ポインタを渡せないというわけではないことにも注意が必要です。

#include <iostream>

template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}

int main()
{
    int v = 100;

    write<int*>(&v);  // OK. T は int*、a は int*
    write(&v);       // OK。T は int*、a は int*
}

実行結果:

006FF9C8
006FF9C8

この場合は、「T」に当てはめられる型が「int*」になるということです。このように、テンプレート仮引数自身に「*」が含まれることもあるし、「T」の方には含めずに、「T*」のように使うこともできます。

関数テンプレートと関数オーバーロード 🔗

関数テンプレートをオーバーロードすることは可能です。関数テンプレートと通常の関数とのあいだでオーバーロードすることも可能です。

#include <iostream>

template <typename T>
void func(T a)
{
    std::cout << "T" << std::endl;
}

template <typename T>
void func(T* p)
{
    std::cout << "T*" << std::endl;
}

void func(const char* s)
{
    std::cout << "const char*" << std::endl;
}


int main()
{
    int v = 100;
    char s[] = "abc";

    func(v);
    func(&v);
    func(s);

    const int cv = 100;
    const char cs[] = "abc";

    func(cv);
    func(&cv);
    func(cs);
}

実行結果:

T
T*
T*
T
T*
const char*

オーバーロードされている関数を呼び出す際に、どの関数が選択されるのかを決定するルールは、すでに前章で説明しています。前章の時点では、関数テンプレートに関する考慮は入っていなかった訳ですが、前章で見たルール自体には変化はありません。このルールの手前に、新たな段階が加わります。

一応、以下に流れを簡略化したものを書いておきますが、詳細を知る必要はまずありません。ルールはプログラマーの感覚にできるだけ沿うように設計されています。もちろん感覚に合わない結果になることはありますが、そのようなときは、明示的に型を指定するなどして、プログラムの方を工夫した方が良いです。

オーバーロードの中に関数テンプレートが含まれている場合はまず、関数テンプレートの特殊化のパターンを洗い出すことから始めます。つまり、実引数を渡して、呼び出すことが可能なパターンを探し出すということです。

たとえば、2つ目の呼び出し「func(&v)」を考えてみます。「&v」の型は「int*」ですから、仮引数が「T」の関数にも「T*」の関数にも渡せます。そのため、「func<int*>(int)」と「func<int>(int*)」という2つの特殊化が候補に挙がります。

そのあと、候補に挙がった特殊化の中で、もっとも限定的な使い方しかできないものが選択されます。「func(T a)」の方はあらゆる型を渡せますが、「func(T* p)」の方はポインタしか渡せませんから、後者の方が選択されるという訳です。

こうして、関数テンプレートの中の最終候補が決定したら、あとの流れは同じです。最終候補の関数テンプレートと、通常の関数とを合わせて、前章で見たルールで「呼び出し可能な最適関数」を決定します。

「呼び出し可能な最適関数」を決定するときに引数の一致度を調べますが、関数テンプレートに関しては、型が確定済みなので、さらに型を拡張したり、暗黙な型変換を適用したりはしません。

もし、最終候補に挙がった関数テンプレートと通常の関数の中で、引数の一致度が同じになるものがあれば、通常の関数の方が優先されます。

ノンタイプテンプレート仮引数 🔗

クラステンプレートの場合と同様に、関数テンプレートでもノンタイプテンプレート仮引数を使えます。基本的には同じことなので、第8章の解説を参照してください。

テンプレートの実装を記述する位置 🔗

複数のファイルを使ったプログラムでは、関数宣言をヘッダファイルに記述し、定義をソースファイル側に記述しますが、関数テンプレートの場合、これがうまくいきません。

// main.cpp

#include "sub.h"

int main()
{
    write(100);
    write(3.5);
    write('x');
    write("xyz");
}
// sub.cpp

#include <iostream>
#include "sub.h"

template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}
// sub.h

#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

template <typename T>
void write(T a);

#endif

このプログラムは、Visual Studio、clang のいずれでも、リンクに失敗します。たとえば、Visual Studio の場合、次のようなエラーメッセージが出力されます。

error LNK2019: 未解決の外部シンボル "void __cdecl write<int>(int)" (??$write@H@@YAXH@Z) が関数 _main で参照されました。 C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<double>(double)" (??$write@N@@YAXN@Z) が関数 _main で参照されました。   C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<char>(char)" (??$write@D@@YAXD@Z) が関数 _main で参照されました。   C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<char const *>(char const * const)" (??$write@PBD@@YAXQBD@Z) が関数 _main で参照されました。 C:\main.obj CppTest
error LNK1120: 3 件の未解決の外部参照 C:\Debug\CppTest.exe    CppTest

要するに、関数テンプレートwrite が、main.obj (main.cpp をコンパイルして生成されるオブジェクトファイル) から参照されているが、その定義が見つからないということです。

定義は sub.cpp にあるのになぜ見つからないかというと、sub.cpp にあるのは関数テンプレートの定義であって、それが実体化したものではないからです。実体化は、関数テンプレートを使う側が、テンプレート実引数を指定(あるいは推定)しないと行えません。しかし、実体化しようにも、main.cpp からは sub.cpp の中身は見えませんから、それもできません。

構文的な問題があるわけではないので、main.cpp も sub.cpp もコンパイルは成功しますが、リンクの過程でエラーになってしまいます。

コンパイラによっては、この問題が起こらないものもありますが、それはテンプレートの取り扱い方の方針が異なるためです。しかしそういうコンパイラは少数派であり、ほとんどのコンパイラは同じ問題を抱えています。

この問題を解決するには、関数テンプレートの定義をヘッダファイル側に記述することです。そうすれば、関数テンプレートを使っている main.cpp から、関数テンプレートの定義が見えるので、実体化することが可能です。

具体的には、以下のようになります。sub.cpp にある関数テンプレートの定義を sub.h へ移動します。宣言はもはやなくても構わないので削除しています。

// main.cpp

#include "sub.h"

int main()
{
    write(100);
    write(3.5);
    write('x');
    write("xyz");
}
// sub.h

#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

#include <iostream>

template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}

#endif

実行結果:

100
3.5
x
xyz

これは関数テンプレートの大きな欠点の1つです。関数の定義がヘッダファイルに露出してしまうため、実装を隠すことができません。また、それが原因で、実装を少しでも変更すると、使用者側のプログラムの再コンパイルが必要です。

関数形式マクロの代替 🔗

関数テンプレートを使うことによって、コードの形が同一にできるのであれば、型の違いを吸収して関数化できます。これは、C言語の関数形式マクロ(C言語編第28章)を使って、型を問わない共通処理を定義することにも似ています。

関数形式マクロを C++ で使うことは可能ですが、以下のような理由から、できるだけ使うことを避けるべきです。

  1. 引数の型や個数、戻り値が明確でない
  2. MACRO(++a); のような、インクリメントやデクリメントを伴う際の問題を避ける手段が無く、本質的につねに危険である(C言語編第28章
  3. プリプロセスで置換されるので、名前空間の概念と無関係であり、影響範囲が広くなりすぎる

関数テンプレートを使うことによって、こういった問題は解決できます。関数テンプレートは実体化されてしまえば普通の関数ですから、マクロに特有な問題とは無縁です。

ただ、1点だけ気になるのは実行効率です。関数形式マクロであれば、関数呼び出しに関するコストが避けられますから、効率は良いはずです。

【上級】関数呼び出しに関するコストとは、実引数のやり取りのためにスタックを操作する処理や、プログラムカウンタが関数のコードの位置へ移動することで起こる参照の局所性の悪化が代表的です。

実行効率の改善には、インライン関数を利用できるかもしれません。第6章で取り上げたとおり、inline指定子を付加したとしても、インライン展開が強制されるわけではありませんが、意志を示すことに価値はあるかもしれません。


練習問題 🔗

問題① 次のプログラムがコンパイルエラーになる理由を答えてください。

#include <iostream>
#include <string>

namespace {

    struct MyData {
        int v1;
        int v2;
    };

    template <typename T1, typename T2>
    inline void write_max(T1 a, T2 b)
    {
        std::cout << ((a >= b) ? (a) : (b)) << std::endl;
    }

}

int main()
{
    MyData a, b;
    a.v1 = 10;
    a.v2 = 20;
    b.v1 = 20;
    b.v2 = 0;

    write_max(a, b);
}

問題② 次の関数形式マクロを、より安全な方法で置き換えてください。

#define MAX(a,b) ((a) > (b) ? (a) : (b))

問題③ 任意の型の配列から、任意の値を線形探索(アルゴリズムとデータ構造編【探索】第1章)で探す関数テンプレートを作成してください。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 VisualStudio 2015 の対応終了。

 C++編【言語解説】第9章「関数テンプレートとインライン関数」の修正に合わせて、内容更新。

 「関数形式マクロの代替」の項を、C++編での更新に合わせて修正。

 新規作成。



前の章へ (第10章 関数オーバーロードとデフォルト実引数)

次の章へ (第12章 参照)

Modern C++編のトップページへ

Programming Place Plus のトップページへ



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