C++編で扱っている C++ は 2003年に登場した C++03
という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14
-> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++
の学習を始めるのなら、C++11
よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい
C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
テンプレート仮引数に具体的な型を当てはめて、普通のクラスや関数の定義を作成する過程を、テンプレートのインスタンス化といいます。テンプレートとは関係無く、クラスのオブジェクトを作り出すこともインスタンス化と呼ばれるので紛らわしいですが、両者は別物です。
たとえば第20章で、Stack<int> iStack;
のように変数を定義しました。この場合、Stackクラステンプレートのテンプレート仮引数 T に int
を当てはめてテンプレートのインスタンス化が行われ、Stack<int>型のクラスが作成されます。そして、Stack<int>型のオブジェクトを
iStack という名前でインスタンス化している訳です。
クラステンプレート自体が型なのではなく、クラステンプレートをインスタンス化した結果、型が得られるのだと考えてください。
このような形で行われるテンプレートのインスタンス化を、テンプレートの暗黙的なインスタンス化(テンプレートの非明示的なインスタンス化)と呼びます。
このとき、メンバの定義はインスタンス化されていません。メンバの定義は、そのメンバが実際に使用されるときにまで先送りされることになっています。これは重要な仕様で、このおかげで次のような使い方が許されます。
#include <iostream>
#include <string>
template <typename T>
class SizeOperator {
public:
explicit SizeOperator(T& t) :
(t)
mTarget{}
inline typename T::size_type Get() const
{
return mTarget.size();
}
inline void Set(typename T::size_type size)
{
.resize(size);
mTarget}
private:
& mTarget;
T};
class DataStoreArray {
public:
typedef std::size_t size_type;
explicit DataStoreArray(size_type size) :
(new int[size]),
mValueArray(size)
mSize{}
~DataStoreArray()
{
delete [] mValueArray;
}
inline size_type size() const
{
return mSize;
}
private:
int* mValueArray;
size_type mSize;
};
int main()
{
std::string str = "abcde";
(10);
DataStoreArray ds
<std::string> op1(str);
SizeOperator<DataStoreArray> op2(ds);
SizeOperator
.Set(50);
op1std::cout << op1.Get() << std::endl;
// op2.Set(50); // DataStoreArray は resize() を持っていない
std::cout << op2.Get() << std::endl;
}
実行結果:
50
10
SizeOperatorクラステンプレートは、複数のデータを管理するクラスをテンプレート仮引数に指定し、Get、Set というメンバ関数を経由して、そのデータの要素数を取得・設定します。あまり意味がないテンプレートではありますが、これを例に取って説明していきます。
main関数のところを見ると、SizeOperator を std::string と DataStoreArray を使ってインスタンス化しています。
std::string を使った op1 の方では、Setメンバ関数と Getメンバ関数を呼び出しています。これらはそれぞれ最終的に、std::string::resizeメンバ関数(【標準ライブラリ】第2章)と、std::string::sizeメンバ関数を呼び出すことになります。大きさを表現する型 T::size_type については、std::string::size_type があります。結果として、この過程の中に何も問題はありません。
一方、DataStoreArrayクラスを使った op2 の方では、「op2.Set(50);」のところをコメントアウトしていますが、このコメントを外すと、コンパイルエラーになります。これは、SizeOperator<T>::Setメンバ関数のところで、DataStoreArray::resize を必要としますが、そのようなメンバ関数は存在していないためです。
ここで注目すべきことは、「op2.Set(50);」をコメントアウトして、Setメンバ関数を呼び出さないようにしていれば、プログラム全体としては問題がないということです。
つまり、クラステンプレートのメンバの定義は、実際に使用しなければ、インスタンス化を行わないので、テンプレート仮引数に具体的な型を当てはめた結果、不正な部分があったとしても問題ありません。
ただし、メンバの宣言については、クラステンプレート自身がインスタンス化されるタイミングで、インスタンス化されます。そのため、次のプログラムのように、DataStoreArrayクラスが size_type を typedef していなければ、Getメンバ関数の戻り値のところに登場する T::size_type を解決できないため、コンパイルエラーになります。
#include <iostream>
#include <string>
template <typename T>
class SizeOperator {
public:
explicit SizeOperator(T& t) :
(t)
mTarget{}
inline typename T::size_type Get() const
{
return mTarget.size();
}
inline void Set(typename T::size_type size)
{
.resize(size);
mTarget}
private:
& mTarget;
T};
class DataStoreArray {
public:
explicit DataStoreArray(std::size_t size) :
(new int[size]),
mValueArray(size)
mSize{}
~DataStoreArray()
{
delete [] mValueArray;
}
inline std::size_t size() const
{
return mSize;
}
private:
int* mValueArray;
std::size_t mSize;
};
int main()
{
std::string str = "abcde";
(10);
DataStoreArray ds
<std::string> op1(str);
SizeOperator<DataStoreArray> op2(ds); // DataStoreArray は size_type を持っていない
SizeOperator
.Set(50);
op1std::cout << op1.Get() << std::endl;
.Set(50);
op2std::cout << op2.Get() << std::endl;
}
このように、クラステンプレートは、宣言の部分に登場するテンプレート仮引数を置き換えた結果が不正であるとエラーになりますが、定義の中に不正な形が登場してしまっても、実際に使おうとしなければエラーになりません。
次のような方法で、テンプレートをインスタンス化できます。
template class Stack<int>;
この記述によって、Stackクラステンプレートのテンプレート仮引数 T に int型を当てはめて、インスタンス化できます。この方法を、テンプレートの明示的なインスタンス化と呼びます。
この方法を使えば、クラステンプレートのメンバ関数の実装をソースファイル側へ隠せます。
// Stack.h
#ifndef STACK_H_INCLUDED
#define STACK_H_INCLUDED
#include <cstddef>
template <typename T>
class Stack {
public:
typedef T value_type;
explicit Stack(std::size_t capacity);
~Stack();
void Push(const T& data);
void Pop();
inline const value_type Top() const
{
return mData[mSP - 1];
}
inline std::size_t GetSize() const
{
return mSP;
}
inline std::size_t GetCapacity() const
{
return mCapacity;
}
private:
const std::size_t mCapacity;
* mData;
Tint mSP;
};
#endif
// Stack.cpp
#include "Stack.h"
#include <cassert>
template <typename T>
<T>::Stack(std::size_t capacity) :
Stack(capacity),
mCapacity(new T[capacity]),
mData(0)
mSP{
}
template <typename T>
<T>::~Stack()
Stack{
delete [] mData;
}
template <typename T>
void Stack<T>::Push(const T& data)
{
assert(static_cast<std::size_t>(mSP) < mCapacity);
[mSP] = data;
mData++;
mSP}
template <typename T>
void Stack<T>::Pop()
{
assert(mSP > 0);
--;
mSP}
// 明示的なインスタンス化
template class Stack<int>;
template class Stack<std::string>;
Stack.cpp の末尾で、明示的なインスタンス化を行っています。この位置に置くことで、Stack.cpp がコンパイルされたときに、int型版と std::string型版のインスタンスが生成されます。
このように明示的なインスタンス化を行っておけば、プログラム内のどこかで Stack<int> や Stack<std::string> を使う際に、明示的に作られた実体が利用されるようになるため、ヘッダファイルに実装がなくても問題なくリンクできます。
ただし問題もあります。そもそも、明示的なインスタンス化を行うには、クラステンプレートの利用者がどんな型をテンプレート仮引数に当てはめるのかを理解している必要があります。今回の場合、int型と std::string型を使いましたが、double型を使わないという保証が取れるでしょうか? もし利用者が、明示的にインスタンス化されていない Stack<double>型でインスタンス化を行ったら、Stack<double>型版のメンバ関数の実装が見つかりませんから、結局、リンクエラーになります。
また、テンプレートのインスタンスは、翻訳単位ごとに行われるため、Stack.h を #include した2つのファイル (main.cpp と sub.cpp) のそれぞれで、int型を当てはめて暗黙的なインスタンス化を行ったとすると、Stack<int> の実体は、main.cpp をコンパイルしたオブジェクトファイルと、sub.cpp をコンパイルしたオブジェクトファイルのそれぞれに含まれることになります。これは、プログラムサイズの増大につながります。
明示的なインスタンス化によって、Stack<int> の実体が、Stack.cpp をコンパイルしたオブジェクトファイルだけに作られるようになりますから、プログラムサイズの増大を軽減させる効果もあります。
なお、もしも同じ明示的なインスタンス化が、プログラム全体の中で2回以上登場した場合、リンクエラーになります。巨大なプロジェクトの場合、明示的なインスタンス化を書く場所をきちんと管理しないと、この問題に引っかかることがあります。
明示的なインスタンス化は、メンバ関数単位で行うこともできます。たとえば、テンプレート仮引数 T を int型として、Pushメンバ関数だけを選択的にインスタンス化させるには、次のように記述します。
template void Stack<int>::Push(const int &);
クラス全体でないので classキーワードが無くなります。また、テンプレート仮引数 T の部分はすべて、具体的な型に置き換えて記述する必要があります。この例の場合だと、引数は const T& ではなく、const int& にしなくてはいけません。
C++11 では、ある翻訳単位内ではテンプレートのインスタンス化を行わないように抑制する機能が追加されました。これは、extern template と呼ばれます。
extern template class Stack<int>;
このように、明示的なインスタンス化の構文の先頭に extern を付けます。この記述がある翻訳単位内では、この形でのインスタンス化が行われなくなります。
main.cpp と sub.spp の2か所で Stack<int>型を使用しているとしましょう。結局のところ、Stack<int> の実体がどこかに1つあれば問題ないのですが、明示的なインスタンス化をしていなければ、翻訳単位ごとに実体が作られているので、無駄な重複が生まれます。
extern template を使うことで、使い方の難しい明示的なインスタンス化を避けつつも、無駄な重複を排除できます。
たとえば、sub.cpp の方に、extern template を記述しておくことで、sub.cpp から作られるオブジェクトファイルには実体が生成されなくなります。当然、どこかには実体が必要が必要になるので、main.cpp には extern template を記述してはいけません。
テンプレート関数の実体をヘッダ側に置かなければならない場合、実装を変更するたびに、そのヘッダを #include しているすべてのソースが影響を受け、再コンパイルを必要とします。これは巨大なプログラムでは、開発効率に影響を与えます。
そこで、テンプレートの宣言時に exportキーワードを付加することによって、定義を別の場所に書けるようにする機能があります。
しかし、exportキーワードを実装したコンパイラはほとんど存在しません。これは、コンパイラを正しく実装することが、技術的に非常に困難であったためです。この事実を受けて、次の標準規格となった C++11 では廃止されているので、このキーワードについての解説は行いません。
問題① クラステンプレートのメンバ関数の定義が、ヘッダ側に書かれていないと、その関数を使用できないことを確認してください。その後、明示的なインスタンス化を行うことで、この問題を解決できることを確認してください。
「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。
コンパイラの対応状況について、対応している場合は明記しない方針にした。
clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。
VisualC++ 2017 に対応。
clang の対応バージョンを 3.7 に更新。
clang の対応バージョンを 3.4 に更新。
VisualC++ 2012 の対応終了。
VisualC++2010 の対応終了。
VisualC++ 2015 に対応。
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |