関数テンプレートとインライン関数 | Programming Place Plus C++編【言語解説】 第9章

C++編【言語解説】 第9章 関数テンプレートとインライン関数

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/17 対応) の以下の章にあります。

この章の概要

この章の概要です。


関数テンプレート

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

そこで登場するのが、関数テンプレート(テンプレート関数)という機能です。この機能を使うと、1つの関数の実装を、複数の型で使いまわすことが可能になります。これは、未知の型であっても対応できる適用力を持っています。

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

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

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

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

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

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

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

関数テンプレートの定義の中には、通常の関数を実装するのと同じように本体のコードを記述します。本体のコードおよび、仮引数や戻り値のところでは、テンプレート仮引数を使用することができます。通常の関数であれば、「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 として扱わせたいのであれば、テンプレート実引数を明示的に指定することになります。

C++14 であれば、「"xyz"s」のように sサフィックスを付加して、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)」の方はポインタしか渡せませんから、後者の方が選択されるという訳です。

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

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

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

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

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

// 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

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

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++11 (デフォルトテンプレート実引数)

C++11

C++11 では、テンプレート仮引数に、デフォルトテンプレート実引数を与えることが可能になりました。

意外と使いどころが難しい新機能なので、良い例とは言い難いですが、次のように書けます。

#include <iostream>

template <typename RET = double>
RET get_value()
{
    return static_cast<RET>(1.5);
}

int main()
{
    std::cout << get_value() << std::endl;
    std::cout << get_value<int>() << std::endl;
}

実行結果:

1.5
1

C++11 (可変個テンプレート仮引数)

C++11

C++11 では、テンプレート仮引数の個数を可変にできるようになりました。

可変個の任意の型の引数を渡して、すべてを標準出力へ出力する write関数を作ってみます。

template <typename... ARGV>
void write(ARGV... argv)
{
    // 省略
}

int main()
{
    write();
    write('a');
    write(10, "xyz", 0.5);
}

テンプレート仮引数に、「...」が使われています。この「...」を、テンプレート仮引数パックと呼び、可変個(0個以上)のテンプレート仮引数があることを意味します。

なお、「typename」は「class」でも構わないですし、「...」の前後にスペースがあっても構いません。

また、関数の仮引数の方にも「...」が使われており、こちらは、関数パラメータパックと呼びます。これも、可変個(0個以上)の引数があることを意味しています。「ARGV...」となっているように、テンプレート仮引数パックに使った名前とセットになっています。

問題は、こうして定義された関数テンプレートwrite の中身をどう記述すれば良いかです。これが意外と難しいのですが、再帰処理を駆使して記述する必要があります。

#include <iostream>

void write_inner()
{
}

template <typename T, typename... ARGV>
void write_inner(T first, ARGV... argv)
{
    std::cout << first << " ";
    write_inner(argv...);
}

template <typename... ARGV>
void write(ARGV... argv)
{
    write_inner(argv...);
    std::cout << std::endl;
}

int main()
{
    write();
    write('a');
    write(10, "xyz", 0.5);
}

実行結果:


a
10 xyz 0.5

呼び出し順に見ていきましょう。

まず、関数テンプレートwrite が呼び出され、その中で、可変個引数 argv を、write_inner へ渡しています。このときにも「...」を付けておく必要があります。この記号があることで、可変個の値をまとめて取り扱っているのだということを明示する訳です。

ポイントとなるのは、write_inner がオーバーロードされていて2種類ある点です。一方は、引数が無く、関数テンプレートにもなっていない普通の関数です。他方は、可変個引数を持った関数テンプレート版です。

関数テンプレートwrite から、どちらが呼び出されるのかというと、argv が実際に何個の値を持っているかに依ります。これが 0個であれば、引数無し版が呼び出され、1個以上あれば可変個引数版が呼び出されます。

可変個引数版の write_inner は、「...」の付いていない引数 first を持っています。これによって、先頭の1つだけが first として与えられ、残りの可変個部分が argv として与えられることになります。

こうして、最初に関数テンプレートwrite に渡された可変個引数の先頭部分から1つ引きはがされ、標準出力への出力が行われ、残りの可変個引数は再び write_inner へと引き渡されます。結果、再帰的に write_inner が呼び出されていきますが、いずれ可変個引数部分が 0個になり、引数無し版の write_inner が呼び出されて、再帰が完了します。

インライン関数

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

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

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

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

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

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

そこで、関数呼び出しのコストを避ける手段として、インライン関数を使います。インライン関数とは、宣言時に inline指定子を付加された関数のことです。

inline void f();

勿論、関数の本体も記述して、定義にしても構いません。

inline void f() {}

inline指定子は、関数テンプレートの定義にも付加することができます。このような関数テンプレートから実体化された関数は、inline指定子が付加された関数になっています。

template <typenameT>
inline void f(T x) {}

インライン関数は、これを呼び出そうとするすべてのソースファイルに、それぞれまったく同一内容の定義がなければなりません。そのため、複数のソースファイルから呼び出したいのであれば、インライン関数の定義をヘッダファイルに記述して、各ソースファイルからインクルードさせるようにします。

inline指定子は、インライン展開を行うことを要請します。インライン展開とは、関数の本体のコードを、その関数を呼び出している箇所に展開することです。これは、関数形式マクロがしていることと実質的に同じといえます。

インライン展開は多くの場合、コンパイル時に行われますが、リンク時など、ほかのタイミングで行われる可能性もあります。

インライン展開が行われることで、関数呼び出しのコストを避けられます。一方で、呼び出し箇所ごとにコードが重複することになるので、プログラムのサイズは大きくなるかも知れません

インライン関数の本体が小さければ、かえってプログラムサイズが小さくなる可能性はあります。

inline指定子は、インライン展開して欲しいという要請に過ぎず、強制的な指示ではありません。インライン展開することが難しい場面もあるため、要請は無視される可能性があります。インライン展開が行われなかった場合は、通常の関数呼び出しの仕組みが使われます。

ループを含む関数や、再帰呼び出しを行う関数、仮想関数(第27章)では、inline指定子の要請を無視することがあります。

ある呼び出し箇所ではインライン展開され、ほかの呼び出し箇所ではインライン展開されないということも起こり得ます。例えば、インライン関数のメモリアドレスを取得している箇所があると、関数の実体が必要になるため、インライン展開を行わないはずです。しかし、それとは別の箇所でインライン展開可能なら、効率を優先して、インライン展開を行うかも知れません。

反対に、inline指定子を付加していなくとも、通常の最適化の一環として、関数がインライン展開されることもあります

結局、inline指定子を付加してもしなくても、インライン展開は行われるかも知れないし、行われないかも知れないということです。インライン関数を使うことによる多少の弊害もあるため、inline指定子を付けることには慎重であるべきです。

弊害とは、例えば、インライン関数の本体のコードをヘッダファイル側に露出しなければならないため、後から実装を変更すると、呼び出し側の再コンパイルを必要とすることなどがあります。大規模な開発では、プログラムのビルド時間も開発効率に大きく影響します。

以下はインライン関数の使用例です。この章の「テンプレートの実装を記述する位置」で使ったサンプルプログラムを、インライン関数を使って書き換えてみます。

// 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

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

#endif

実行結果:

100
3.5
x
xyz

write関数テンプレートの定義に inline指定子を付加し、実装もヘッダファイル内に記述するようになりました。この関数テンプレートを使用する側は、ヘッダファイルをインクルードします。


練習問題

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

#include <iostream>
#include <string>

struct MyData {
    int value;
    std::string name;
};

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

int main()
{
    MyData a, b;
    a.value = 10;
    a.name = "aaa";
    b.value = 20;
    b.name = "bbb";

    write_max(a, b);
}

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

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

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


解答ページはこちら

参考リンク



更新履歴

'2018/8/18 全体的に見直し修正。
関数テンプレート」の項の内容を細分化して、「複数個のテンプレート仮引数」「テンプレート実引数の推定」に分けた。
テンプレート仮引数と関数の仮引数」の項を追加。
「C++11 (戻り値を後ろに置く構文)」「C++14 (戻り値の型推論)」の項を削除。

'2018/7/19 「インライン関数」の項を全面的に書き換えた。

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。
Xcode 8.3.3 を clang 5.0.0 に置き換え。

≪更に古い更新履歴を展開する≫



前の章へ(第8章 関数オーバーロード)

次の章へ(第10章 言語間の連携)

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS