組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

3.9 テンプレートの裏側

C++のテンプレートは非常に強力な機能です.標準C++ライブラリに取り入れられたSTL(Standard Template Library)も,テンプレート無しには語ることができません.しかし,C++の入門書の多くでは,テンプレートについて詳しく解説されることは稀です.そのためか,それなりに経験を積んだC++プログラマーであっても,テンプレートに対する理解は必ずしも深くはないようです.

テンプレートに関する概要はすでに第1章で解説しましたので,ここでは,テンプレートがC++コンパイラにどのように解釈され,そして,どのようなコードが生成されるかを中心に解説することにします.

3.9.1 テンプレートの具現化

テンプレートというのは,文字どおりプログラムの「ひな形」ですので,それを実際に使う際には実体(Instance)を作る必要があります.テンプレートの実体を作ることを「具現する」(Instantiate)といい,具現することを「具現化」(Instantiation)といいます.

Cの場合,外部結合を持つ関数やオブジェクトの定義は,プログラム中でただ1カ所だけ記述されます.C++でも原則は同じですが,テンプレートは例外的に複数の翻訳単位で同じ記述を行うことができます*1.しかし,関数テンプレートや,クラステンプレートのメンバー関数や静的データメンバーは,インライン関数を除けば,たとえ同じ定義が複数の翻訳単位で記述されたとしても,コンパイル~リンク後に生成されるプログラム中ではただ1つの実体を持つことになります.そのため,メモリマップを厳密に管理する場合には注意が必要です.

【f.hpp】

// f.hpp
#ifndef F_HPP_
#define F_HPP_
template <typename T>
T f(T arg)
{
     …
}
#endif  // F_HPP_

【a.cpp】

// a.cpp
#include "f.hpp"
void a()
{
     …
    f(123); // ← f<int>(int)を具現化 
     …
}

【b.cpp】

// b.cpp
#include "f.hpp"
void b()
{
     …
    f(456); // ← f<int>(int)を具現化 
     …
}

上のコードのメモリレイアウトは図3.6のようになります.

●図3.6 上記コードのメモリレイアウト

図3.6 上記コードのメモリレイアウト

テンプレートの具現化には2種類の方法があります.1つは暗黙的な具現化で,関数テンプレートを呼び出す際に指定した実引数の型に基づいて暗黙的に起きる具現化です.もう1つは明示的な具現化で,特別な宣言によって起きる具現化です.

template <typename T> void func(T arg)
{
     …
}
int main()
{
    unsigned long x = 123UL;
    func(x); // ← 暗黙的な具現化 
    return 0;
}
template void func<int>(int arg); // ← 明示的な具現化 

このうち,少なくとも暗黙的な具現化を用いた場合には,具現された実体は,それを記述した翻訳単位の他のコード付近に配置されるかどうかはまったくわからなくなります.一方,明示的な具現化による実体は,それを記述した翻訳単位の他のコード付近に配置される可能性が出てきます.ただし,言語規格でそれが保証されているわけではないので,実際にはコンパイル結果を調べてみる必要があります.筆者が知るかぎりでは,明示的な具現化を用いた場合も,暗黙的な具現化を用いた場合と同様に,それを記述した翻訳単位の他のコード付近に配置されないことのほうが多いようです.

*1 テンプレートのほか,インライン関数も例外になります.

3.9.2 テンプレートは静的に解決される

テンプレートは,前処理と同様,すべてコンパイル時に静的に解決されます.そのため,テンプレート実引数には,汎整数定数式や静的な型しか与えることができません.ただし,前処理とは異なり,式の評価方法は通常の定数式と同じです.前処理では,すべての整数はlong型として扱われ,sizeof演算子などは使えませんでしたが,テンプレート実引数ではそれらも普通に使用することができます.

静的,すなわちコンパイル時にテンプレートが解決されるということは,テンプレートが展開される過程をステップ実行させるようなこともできなければ,printfデバッグを行うことも,assertマクロを使うこともできません.しかし,テンプレートの展開時にエラーが発生した場合,多くのコンパイラはその展開過程をエラーメッセージとして出力します.Cでは,エラーメッセージはおおむね1行程度の簡潔なものでしたが,C++でテンプレートを使ったときに膨大なエラーメッセージが出るのはそのためです.

では,テンプレートが具体的にどのタイミングで解決されるかについて確認してみましょう.はじめに,C++コンパイラがコンパイルを行う手順「翻訳過程」をざっと把握しておく必要があります.C++の翻訳過程を簡単に示すと,下記のようになります.

  1. 物理的文字から基本ソース文字集合の文字への変換
  2. 物理行から論理行への変換
  3. 前処理字句と空白類(コメント含む)の列への分解
  4. 前処理指令の実行
  5. 文字(列)リテラル中の文字から実行文字集合の文字への変換
  6. 文字列リテラルの連結
  7. 前処理字句を字句に変換し,意味解析を行ったうえで翻訳
  8. 翻訳済み翻訳単位と具現化単位を結合
  9. 外部オブジェクトと外部関数への参照を解決

翻訳過程というのは,C++の構文規則を解決するうえでの優先順位のことです.実際にコンパイラがこの手順で解決しているかどうかはわかりませんが,見かけ上はそのように振る舞わなければなりません.

翻訳過程のうち,1~6の過程が前処理(プリプロセス)に相当します.そして,7が狭義のコンパイルに,8~9はリンクに相当します.このうち,コメントが取り除かれるのは3であり,マクロが展開されるのは4です.それに対して,テンプレートが展開されるのは7です.8の中の「具現化単位」というのは,具現されたテンプレートは,前述したように,それが記述された翻訳単位の他の要素からは分離されるため,翻訳済み翻訳単位とは別の扱いになっています.

3.9.3 テンプレートによるプログラムサイズの肥大化

テンプレートを用いた場合の弊害としてよく取り上げられるのが,プログラムサイズの肥大化です.テンプレートは,指定したテンプレート実引数の種類だけ異なる実体が具現されるためです.

int abs(int arg);
long abs(long arg);

たとえば,上記のように多重定義した場合には,sizeof(int) > sizeof(short)となる処理系では,char型やshort型の実引数を渡した場合には,すべてint版が呼び出されます.

しかし,次のように,関数テンプレートとして定義した場合には,char型の実引数を渡せばabsが,short型の実引数を渡せばabsが,それぞれ具現されてしまいます.もともとインライン関数にするような小さな関数であれば,実質的な問題はないでしょう.しかし,大きな関数の場合には,サイズの肥大化が深刻な問題となります.

template <typename T>
T abs(T arg);

従来どおりのCの開発で,扱う型や処理,数値が,ほんの一部だけ異なる比較的大きな関数を記述する場合,普通はどうするでしょうか? 最も安易な方法は,コピー&ペーストを行って必要なだけ関数を増やし,該当部分だけ書き換えるというものです.確かに状況によってはそのほうがよい場合もありますが,普通は,異なる部分を別関数に切り出すなど,共通部分を再利用する方法を考えることでしょう.

テンプレートを用いた場合でも事情は同じです.プログラムサイズの肥大化を防ぐには,共通部分と異なる部分に分解し,異なる部分だけにテンプレートを適用するほうがよいのです.上記の例でいえば,次のようにすることで,プログラムの肥大化は防げるのです.

int abs(int arg);
long abs(long arg);
template <typename T>
inline T abs(T arg)
{
    return abs(static_cast<int>(arg));
}

ところで,テンプレートを用いたほうがプログラムサイズが小さくなることもあります.次のような状況を考えてみましょう.

struct A
{
    int f(int);
    int g(int);
    int g(int);
};
int A::f(int arg)
{
     …
}
int A::g(int arg)
{
     …
}
int A::h(int arg)
{
     …
}

この場合,A::f,A::g,A::hは,それが実際に呼び出されるかどうかにかかわらず,必ず実体が生成されてしまいます.一般的なC++のソース管理では,同じクラスに属しているメンバー関数は同じソースファイルに記述するか,せいぜい数個のソースファイルに分割する程度です.そのため,テンプレートを使用しないクラスの場合,実際には使用しないメンバー関数ももれなく付いてきてしまうのです.

template <class T>
struct A
{
    int f(int);
    int g(int);
    int g(int);
};
template <class T>
int A<T>::f(int arg)
{
     …
}
template <class T>
int A<T>::g(int arg)
{
     …
}
template <class T>
int A<T>::h(int arg)
{
     …
}

それに対して,上記のようにクラステンプレートにした場合,A::f,A::g,A::hは,実際にそれらのメンバー関数を参照するようなコードを書かないかぎり,実体が生成されることはありません.そのため,すべてのメンバー関数をヘッダファイルの中に記述しておいたとしても,本当に必要なコード以外は実体が生成されないのです.

これは,クラステンプレートのメンバー関数に限ったことではなく,クラステンプレートの静的データメンバーについても同じことがいえますし,関数テンプレートについてもやはり同じことがいえます.