テンプレートの実装をヘッダに書かなければならない理由

ロベールのC++入門講座 09-03

ロベール本ではテンプレートの実装はヘッダに書かなければならないとさらりと書かれていましたが、何故そうしないといけないのかが本を読むだけでは今一良くわからなかったので色々調べてみたところ、理由がわかりました。

まず宣言と定義をファイルを分けて実装した場合、どういった状態になるかを見てみましょう。

// -- csample.h --
#ifndef __CSAMPLE_H__
#define __CSAMPLE_H__

template<class T> void template_swap(T& a, T&b);

#endif
// -- csample.cpp --
#include "csample.h"

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}
// -- main.cpp --
#include "csample.h"
#include <iostream>

int main () {
    int a = 10;
    int b = 20;
    
    template_swap(a,b);
    
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    
    return 0;
}

csampleヘッダにはテンプレート関数の宣言だけ書いておき、csample.cppファイルに実装を定義しています。

さてこれをコンパイルするとどうなるか?

$ cl /W4 /EHs main.cpp csample.cpp
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

main.cpp
csample.cpp
コードを生成中...
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj
csample.obj
main.obj : error LNK2019: 未解決の外部シンボル "void __cdecl template_swap<int>(
int &,int &)" (??$template_swap@H@@YAXAAH0@Z) が関数 _main で参照されました。
main.exe : fatal error LNK1120: 外部参照 1 が未解決です。

コンパイル自体は通ったものの、リンクエラーが出ています。

どうやらmain.objで使用しているint型のtemplate_swapが見つからないようです。

template_swapはcsampleヘッダで宣言してるしcsample.cppには実装も書いてあるのに何故こんなエラーになるのでしょうか?

答えは分割コンパイルにあります。

分割コンパイルはあくまでもファイル単位でコンパイルします。

自分の脳内で順を追って考えてみます。

まずmain.cppのコンパイルから考えてみましょう。main.cppの中で

template_swap(a,b);

と呼び出している部分がありますね。これはcsampleヘッダに

template<class T> void template_swap(T& a, T&b);

と宣言してあるので、template_swapの実装自体はまだ見えてないものの、template_swapの宣言が存在しているのでコンパイルが通り、無事main.objファイルが生成されます。

次にcsample.cppのコンパイルではtemplate_swapの実装が存在しています。

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}

ですが、このtemplate_swapを使っている処理がどこにも見当たらないのでこのtemplate_swap関数のテンプレートの実体化は行われません。

なので何一つ実体化が行われない状態でcsample.objファイルが生成されます。

さぁそれぞれのファイルの分割コンパイルが終わりました。

main.objではint型のtemplate_swapの実装を探しますが、csample.objは単体でコンパイルした時点ではどこからも参照されてなかったので実体化が行われていないのでint型のtemplate_swapは存在せず、結果未解決のシンボルが発生するということになります。

酷い話ですね。

こういう理由から、テンプレートを使う場合はその実装もヘッダで書かないといけないというわけなんですね。

一応、明示的実体化と呼ばれる方法で宣言と定義を分けて実装することもできるようです。

やり方は簡単。上記の例で言うと、csample.cppをコンパイルする時点でint型のtemplate_swapが実体化されるように予めint型のtemplate_swapを使いますよーと宣言しておけばいいのです。

// -- csample.cpp --
#include "csample.h"

// int型のtemplate_swapを明示的実体化
template void template_swap(int& a, int&b);

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}

このようにしておけば、csample.cpp単体で見たときにもint型のtemplate_swapが使われることがわかるので実体化され、main.objから利用する際に見つけることができるようになります。

ただしこのやり方の欠点としては、そもそもどんな型でも処理させるためのテンプレートという機構なのに、どの型で使われるかを予め宣言しておかないといけないという本末転倒ぶりがやば過ぎてあまり使う気がしないです。

やはりヘッダに実装も全部書いてしまうのが一番良いのだと思います。

そういう意味ではテンプレートを利用するクラスや関数を実装する場合は、なるべく小さなものにすべきなんでしょうね。

テンプレート引数の記述を省略する

ロベールのC++入門講座 09-07

タイトルがちょっとわかり辛いですが、例を見ればすぐわかると思います。

まず以下の処理を見てください。

template<class T>
class CSample {
public:
    CSample<T>& operator=(const CSample<T>& obj) {
        // 中身は適当。とりあえず自分自身を返しておく
        return *this;
    }
};

戻り値や引数に自分自身のクラスを指定する場合、

CSample<T>

といったようにをつけて書くのですが、実はの部分は省略が可能なのです。

template<class T>
class CSample {
public:
    CSample& operator=(const CSample& obj) {
        // 中身は適当。とりあえず自分自身を返しておく
        return *this;
    }
};

省略してもコンパイルは通ります。

これはこのクラスがtemplateであることが既にハッキリしているので省略が可能となります。

しかしinlineせずに定義した場合は注意が必要です。

#include <iostream>

template<class T>
class CSample {
public:
    // 宣言だけ
    CSample& operator=(const CSample& obj);
};

// 定義する
template<class T>
CSample<T>& CSample<T>::operator=(const CSample& obj) {
    return *this;
}

この場合、が省略できるのは引数の部分だけとなります。

何故かというと、まず「CSample::operator=」はをつけないとCSampleの解釈ができないのです。CSampleはクラステンプレートであってクラスではないのでCSampleといったように明示的書いてやる必要があります。

そして一度CSampleと書けば以降のCSampleはクラステンプレートであるということがハッキリするので引数のCSampleにはを省略することができます。

同じ理由から戻り値のCSampleはまだCSampleがまだハッキリしていない状態なのでCSampleを書かないといけないようです。

省略可能であることはわかりましたが、色々と面倒臭いので基本的にはを書いておいたほうが安全だとは思います。

関数テンプレートにテンプレート引数のデフォルト値が渡せない理由

ロベールのC++入門講座 09-08

前にテンプレートの勉強をした時に疑問に思ってた部分ですが、ロベール本で少し言及がありました。

実は関数テンプレートのテンプレート引数の場合はデフォルト引数をとることができません。

これはおそらく、関数テンプレートの場合は基本的に引数から型を推測するため、デフォルト引数が必要になる状況自体がほとんどないからではないかと思います。

なるほど。確かにクラステンプレートと違って関数テンプレートの場合はその関数の引数から自動判別して使うやり方が多いのでデフォルト引数を設定する必要性があまり感じられず、できないようにしたのかもしれませんね。

でもあったらあったで便利だったとは思いますが。

テンプレートの実装のエクスポート

ロベールのC++入門講座 09-08

テンプレートの実装をヘッダに書かなければならない理由 - (void*)Pないと

の続きです。

前回の記事でテンプレートの実装はヘッダで書かなければならないと言っていましたが、実はexportという指定子(?)を使えばヘッダに宣言だけを書くことが可能になるようです。

export template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}

これで前に懸念していた問題は全て解決です!とても素晴らしいです。

・・・でも、このexportという指定子は現存するコンパイラの殆どでまだ実装されてないらしく、コンパイルが通りませんorz

今僕が使っているVC2008でもダメでした。残念ながらexportをがんがん使うのはまだまだ未来の話になりそうですね。

アロケータ

http://www.geocities.jp/ky_webid/cpp/library/028.html

ここはざっくり読み進めるだけにしておきます。

基本的にはplacement newを使ってメモリプールから領域を確保することでnew/deleteの処理速度の低下を防ぐ目的だといえば早いでしょうか。(違うかもしれませんが)

vector等のコンテナでもアロケータが使われており、テンプレート引数の第二引数で別のアロケータも使うことが可能なようです。

ただし、通常独自に定義したアロケータを使うことは稀なようなので使わないといけない場面にきたらまたちゃんと勉強しようと思います。

入出力ストリーム

http://www.geocities.jp/ky_webid/cpp/library/029.html
http://www.geocities.jp/ky_webid/cpp/library/030.html

入出力ストリームの話です。ちょうど今ロベールで勉強してるところですね。

実は今まで使用していたstd::coutというのはstd::ostreamクラスのインスタンスだったようです。そういえばcoutが何であるかをあまり深く考えていませんでした。

またstd::cinもstd::istreamクラスのインスタンスになります。

ざっくり仕様を押えておきたいと思います。

std::swap、std::max、std::min

http://www.geocities.jp/ky_webid/cpp/library/034.html

補助的な関数です。


std::swap関数


値の入れ替えをするだけの関数です。

#include <iostream>
#include <algorithm>
using std::cout; using std::endl;

int main () {
    int a = 10;
    int b = 20;
    
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    std::swap(a,b);
    
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    return 0;
}
$ main
a = 10
b = 20
a = 20
b = 10

std::max関数


渡された二つの値から大きい方の値を返します。

#include <iostream>
#include <algorithm>
using std::cout; using std::endl;

int main () {
    int a = 10;
    int b = 20;
    
    int result = std::max(a,b);
    
    cout << result << endl;
    
    return 0;
}
$ main
20

また、第三引数に関数ポインタ(及び関数オブジェクト)を渡せば独自に比較処理を通すこともできます。

#include <iostream>
#include <algorithm>
using std::cout; using std::endl;

bool comp_ptr (int* a, int* b) {
    return *a < *b;
}

int main () {
    int a = 10;
    int b = 20;
    
    // ポインタで渡した場合の処理
    int* result = std::max(&a,&b,comp_ptr);
    
    cout << *result << endl;
    
    return 0;
}
$ main
20

std::min関数


渡された二つの値から小さい方の値を返します。

使い方はstd::maxとまったく同じです。


ちなみにstd::maxとstd::minはVC6.0には存在しないそうです。注意が必要ですね。

別名の名前空間を定義する

ロベールのC++入門講座 08-13

namespaceによる名前空間の別名を定義することができます。

#include <iostream>

namespace FooBarBaz {
    int i = 10;
}

// 別名の定義
namespace FBB = FooBarBaz;

int main () {
    // アクセスできる
    std::cout << FBB::i << std::endl;
    return 0;
}
$ main
10

別名の定義ができていますね。

またネストされた名前空間であっても別名を定義できます。

// ネストされた名前空間
namespace Foo {
    namespace Bar {
        namespace Baz {
            int i = 10;
        }
    }
}

// 別名の定義
namespace FBB = Foo::Bar::Baz;

非常に長い名前空間を扱う場合に省略形を定義するという感じで使うこともあるかもしれませんね。

無名の名前空間

ロベールのC++入門講座 08-13

namespaceを名前無しで定義すると無名の名前空間になります。

namespace {
    int i = 100;
}

こうして定義されたi変数はスコープがファイル内(ブロック内ではないことに注意)に限られます。

// -- main.cpp --
#include <iostream>

namespace {
    int i = 100;
}

int main () {
    // iはファイルスコープなのでここでもアクセスできる。
    std::cout << i << std::endl;
    return 0;
}
// -- main2.cpp --
extern int i;
int j = i; // i変数はmain.cppのファイルスコープなのでアクセスできない
$ cl /W4 /EHs main.cpp main2.cpp
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

main.cpp
main2.cpp
コードを生成中...
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj
main2.obj
main2.obj : error LNK2019: 未解決の外部シンボル "int i" (?i@@3HA) が関数 "void _
_cdecl `dynamic initializer for 'j''(void)" (??__Ej@@YAXXZ) で参照されました。
main.exe : fatal error LNK1120: 外部参照 1 が未解決です。

ちなみにある名前空間の中で無名の名前空間を定義した場合は、そこに存在しているかのように振舞います。

#include <iostream>

namespace Foo {
    namespace {
        int i = 100;
    }
}

int main () {
    std::cout << Foo::i << std::endl;
    return 0;
}
$ main
100

こんな感じですね。

ただちょっと理解があいまいかもしれないのでもう少し調べる必要がありますね。

テンプレート定義時のtypenameとclassの違い

ロベールのC++入門講座 09-02

template<typename T>
...

template<class T>
...

において、動作の違いはまったくないそうです。

ただし古いコンパイラではtypenameだと対応していない可能性があるらしいです。

とりあえず本ブログではタイプ量の少ないclassを使っていきたいと思います。