こんにちは。田原です。
CRTPとはCuriously Recurring Template Patternの頭文字で「奇妙に再帰したテンプレートパターン」とも訳されるようです。「奇妙」は「おかしな」とか「変な」のニュアンスではなくて「不思議な」が近いように思います。C++の様々な機能と同様、知らないと困るものではないですが、デメリットなくプログラムを高速化できるケースがあるので、有用なテクニックの1つです。(もちろん、他の機能同様使えないケースもありますので選択肢の1つと捉えて下さい。)
1.基本構造
「パターン」と呼ばれるように、要するにプログラムの定型的な構造です。CRTPの構造自体は単純です。
クラス・テンプレートを基底クラスとし、そのテンプレート・パラメータに派生クラスを与えるという構造です。
template<class tDerived>
class CrtpBase
{
};
class CrtpDerived : CrtpBase<CrtpDerived>
{
};
第一印象は、不思議な構造と言うか何の役に立つのだろうと私は感じました。
- 基底クラス・テンプレートに派生クラスを与えている以上「動的ポリモーフィズム」としては使えません。
基底クラスへのポインタは、派生クラス毎に異なる型になるため、同じ基底クラス型のポインタで管理できませんので。(例えば1つのstd::vectorで「CTRP形式の異なる派生クラス」のインスタンスを管理出来ないという意味です。次の2.も参照下さい。) -
「静的ポリモーフィズム」のようにも見えますが、無関係です
ワザワザ派生しなくても、単に同じシグニチャ(名前と引数と戻り値が同じ)関数を定義しておけば、同じ記述で呼び出せます。#include <iostream> #include <vector> struct Foo { void print() { std::cout << "This is Foo.\n"; } }; struct Bar { void print() { std::cout << "This is Bar.\n"; } }; int main() { Foo aFoo; Bar aBar; aFoo.print(); aBar.print(); // std::vector<???> aFooBarList; // ???に何を書く? }1つのコンテナで管理できないという事実も変わりません。
FooとBarが、それぞれCtrpBase<Foo>、CtrpBase<Bar>からpublic継承されていても同様です。実体化されていないクラス・テンプレートは ??? に書けません(*1)ので、実体化したCtrpBase<Foo>*、CtrpBase<Bar>*のどちらかを書くしか無いです。書かなかった方は aFooBarList で管理できません。
(*1) クラス・テンプレートをstd::vectorの型として指定できない
クラスは「型」ですが、クラス・テンプレートは雛形であり「型」ではないのです。std::vector<T>のTには「型」を指定しますから、クラス・テンプレートを書くことができません。
因みに、テンプレート・パラメータとして、今まで型パラメータと非型パラメータの2種類を解説しましたが、他に「テンプレート」パラメータもあります。テンプレート・テンプレート・パラメータと呼ばれています。
2.ではどんな時に便利なのか?
一般に、基底クラスでメンバ変数やメンバ関数を定義するとそれらは全て派生クラスへ継承されます。
そして、通常のパターンでは基底クラスは派生クラスの情報を持っていないため、派生クラスへの参照や派生クラスのメンバ関数呼び出し等を実装することができません。
CRTPは、基底クラス・テンプレートに派生クラスの型を与えることでそれらをできるようにしています。
2-1.例1:比較演算子の導出
2-1-1.比較演算子の導出とは?
比較演算子はかなりたくさんありますね。’==’, ‘!=’, ‘<‘, ‘>’, ‘<=’, ‘>=’
そして、これらはoperator<があれば残りを全て論理演算のみで導出できます。
例えば、簡単な年月日のみを記録する日付クラスで見てみましょう。
#include <iostream>
#include <iomanip>
#include <cstdint>
#include <tuple>
struct Date
{
uint16_t mYear;
uint8_t mMonth;
uint8_t mDay;
Date(uint16_t iYear, uint8_t iMonth, uint8_t iDay) :
mYear(iYear), mMonth(iMonth), mDay(iDay)
{ }
bool operator<(Date const& iRhs) const
{
return std::tie(mYear, mMonth, mDay) < std::tie(iRhs.mYear, iRhs.mMonth, iRhs.mDay);
}
// operator<から導出
bool operator>(Date const& iRhs) const { return (iRhs < *this); }
bool operator==(Date const& iRhs) const { return !(*this < iRhs) && !(iRhs < *this); }
bool operator!=(Date const& iRhs) const { return (*this < iRhs) || (iRhs < *this); }
bool operator<=(Date const& iRhs) const { return !(iRhs < *this); }
bool operator>=(Date const& iRhs) const { return !(*this < iRhs); }
};
int main()
{
Date aToday(2018, 2, 18);
Date aTomorrow(2018, 2, 19);
std::cout << std::boolalpha;
std::cout << "aToday < aTomorrow = " << (aToday < aTomorrow) << " : ";
std::cout << "aToday < aToday = " << (aToday < aToday) << "\n";
std::cout << "aToday > aTomorrow = " << (aToday > aTomorrow) << " : ";
std::cout << "aToday > aToday = " << (aToday > aToday) << "\n";
// 以下略
}
aToday < aTomorrow = true : aToday < aToday = false aToday > aTomorrow = false : aToday > aToday = false aToday == aTomorrow = false : aToday == aToday = true aToday != aTomorrow = true : aToday != aToday = false aToday <= aTomorrow = true : aToday <= aToday = true aToday >= aTomorrow = false : aToday >= aToday = true
ある時、大小比較を文字列の長さで定義したいケースが発生したので次の定義を作りました。
struct SizeString : public std::string
{
using std::string::string;
bool operator<(SizeString const& iRhs) const
{
return size() < iRhs.size();
}
// operator<から導出
bool operator>(SizeString const& iRhs) const { return (iRhs < *this); }
bool operator==(SizeString const& iRhs) const { return !(*this < iRhs) && !(iRhs < *this); }
bool operator!=(SizeString const& iRhs) const { return (*this < iRhs) || (iRhs < *this); }
bool operator<=(SizeString const& iRhs) const { return !(iRhs < *this); }
bool operator>=(SizeString const& iRhs) const { return !(*this < iRhs); }
};
2-1-2.比較演算子の導出にCRTPを使う
他の比較演算子も必要な場合、残りの比較演算子を全てコピペしてパラメータの型が異なるので1つ1つ修正します。なかなか面倒ですね。そのような時、CRTPの出番です。
まずは次のような導出する比較演算子だけを定義したクラス・テンプレートを定義します。
その時、派生クラスをパラメータに取ることで、派生クラスのoperator<を呼び出させます。
このように基底クラスから派生クラスのメンバ関数を直接呼び出せることがCRTPの特長の1つです。
template<class tDerived>
struct CompareBase
{
// operator<から導出
bool operator>(tDerived const& iRhs) const
{
return (iRhs < static_cast<tDerived const&>(*this));
}
bool operator==(tDerived const& iRhs) const
{
return !(static_cast<tDerived const&>(*this) < iRhs)
&& !(iRhs < static_cast<tDerived const&>(*this));
}
bool operator!=(tDerived const& iRhs) const
{
return (static_cast<tDerived const&>(*this) < iRhs)
|| (iRhs < static_cast<tDerived const&>(*this));
}
bool operator<=(tDerived const& iRhs) const
{
return !(iRhs < static_cast<tDerived const&>(*this));
}
bool operator>=(tDerived const& iRhs) const
{
return !(static_cast<tDerived const&>(*this) < iRhs);
}
};
比較演算子を自動的に導出したいクラスについて、次のように使います。
struct Date : public CompareBase<Date>
{
uint16_t mYear;
uint8_t mMonth;
uint8_t mDay;
Date(uint16_t iYear, uint8_t iMonth, uint8_t iDay) :
mYear(iYear), mMonth(iMonth), mDay(iDay)
{ }
bool operator<(Date const& iRhs) const
{
return std::tie(mYear, mMonth, mDay) < std::tie(iRhs.mYear, iRhs.mMonth, iRhs.mDay);
}
};
struct SizeString : public std::string, public CompareBase<SizeString>
{
using std::string::string;
bool operator<(SizeString const& iRhs) const
{
return size() < iRhs.size();
}
};
たいへんお手軽に複数のクラスで比較演算子群を実装できました。
ダウンキャストについて
static_castを使ってダウンキャストしています。適切にCRTPに従っていればstatic_castで問題ありませんが、使い方を誤ると危険なstatic_castになります。心配な場合はdynamic_castすることも可能です。その場合は、得失を検討した上で仮想関数も設けて下さい。
2-2.仮想関数を用いる場合との比較
「基底クラスから派生クラスのメンバ関数を呼び出す」と言えば、仮想関数ですね。
実際、同様なことを仮想関数を用いて実現できます。
- CompareBaseを通常のクラスとする
- CompareBaseのoperator<を純粋仮想関数として定義する
- 派生クラス、基底クラスの両方の比較演算子のパラメータを基底クラスで受け取る
- 派生クラスのoperator<はパラメータを派生クラスへstatic_castして処理する
ここはdynamic_castの必要はまずないと思います。自分の基底クラスをパラメータに指定するのですから、ダウンキャスト可能です。しかし、パラメータの型を間違うと危険なstatic_castになるので、そのバグを確実に検出したい場合はdynamic_castを使うのも有りと思います。
struct CompareBase
{
virtual bool operator<(CompareBase const& iRhs) const = 0;
// operator<から導出
bool operator>(CompareBase const& iRhs) const { return (iRhs < *this); }
bool operator==(CompareBase const& iRhs) const { return !(*this < iRhs) && !(iRhs < *this); }
bool operator!=(CompareBase const& iRhs) const { return (*this < iRhs) || (iRhs < *this); }
bool operator<=(CompareBase const& iRhs) const { return !(iRhs < *this); }
bool operator>=(CompareBase const& iRhs) const { return !(*this < iRhs); }
};
struct Date : public CompareBase
{
uint16_t mYear;
uint8_t mMonth;
uint8_t mDay;
Date(uint16_t iYear, uint8_t iMonth, uint8_t iDay) :
mYear(iYear), mMonth(iMonth), mDay(iDay)
{ }
bool operator<(CompareBase const& iRhs) const
{
auto aRhs = static_cast<Date const&>(iRhs);
return std::tie(mYear, mMonth, mDay) < std::tie(aRhs.mYear, aRhs.mMonth, aRhs.mDay);
}
};
struct SizeString : public std::string, public CompareBase
{
using std::string::string;
bool operator<(CompareBase const& iRhs) const
{
auto aRhs = static_cast<SizeString const&>(iRhs);
return size() < aRhs.size();
}
};
同じことができるのであれば、どちらを使った方が良いか悩みますね。実際、一長一短があるのでここは悩みどころです。
- 仮想関数のメリット
必要な場合には動的ポリモーフィズムを使えます。基底クラスへのポインタを介して、全ての比較演算子を使えます。
逆にCRTPではそれはできません。 -
仮想関数のデメリット
① 仮想関数テーブル(vtable)経由で派生クラスのoperator<を呼び出しますので微妙に遅いです。これはジャンプ先の取得に1クッション入るだけですので、これだけなら大きくは劣化しない筈です。
② しかし、仮想関数テーブルを経由するのでインライン展開されません。インライン展開されないということは最適化も劣化します。今回のように非常に短い関数の場合、意外に大きく性能が落ちる場合もあると思います。
従って、動的ポリモーフィズムを使う必要がない時はCRTPを使った方が好ましいです。
上記のサンプルのようなDateクラスとSizeStringクラスをポリモーフィックに管理するケースは少ないでしょうから、この状況も少なくないと思います。
2-3.例2:シングルトンの量産
先の例では、基底クラスから派生クラスのメンバ関数を呼び出しました。
基底クラスに派生クラスへの参照メンバを定義することもできます。その例としてシングルトンを取り上げてみます。
この例ではstaticメンバ変数を定義しますが、非staticメンバ変数も可能です。
シングルトンとは
シングルトンはインスタンスが1つしか作れないように制限されたクラスです。
つまり、普通のクラスの方が機能は豊富(複数のインスタンスを作れる)ですので、通常のクラスでも足りますが、インスタンスを1つしか作りたくない場合もあります。
例えば、トラブル追跡用のログ出力クラスのインスタンスはプログラム内に1つだけ存在させたい場合もあります。しかし、C++のデフォルト動作はコピーですので油断や見落としでコピーになる可能性も無視できません。そのちょっとした油断でログ用インスタンスが複数生成されるとログが正常にとれなくなります。トラブル追跡用のログ自体がトラブルで可笑しくなるとは目も当てられません。
そのような場合はシングルトンとし、自分も含めて誰かが間違ってコピーしたらコンパイルエラーになるように仕込んでおくと安心です。
2-3-1.簡単なシングルトン
シングルトンの作り方はいくつかありますが、ここでは次の方法で作ります。
- 唯一のインスタンスをgetInstance()関数のstaticローカル変数として獲得する。
- インスタンスを増やすメンバ関数はprivate定義したり、deleteしたりする。
- アプリ起動時にインスタンスを自動生成する。
この3.はお勧めオプションです。- シングルトンを複数のスレッドから同時に生成すると生成が競合します。
アプリ起動時はシングルスレッドで動作するのでこのタイミングで生成すれば競合を回避できます。 - シングルトンは開放しないような使い方が多いですので、アプリ稼働中に生成されるとそのメモリは開放されません。これがメモリを分断しますので、最初に生成して軽減します。
- シングルトンを複数のスレッドから同時に生成すると生成が競合します。
#include <iostream>
class Singleton
{
static Singleton& sInstance;
// コンストラクタをprivateに定義する
Singleton()
{
std::cout << "singleton()\n";
}
public:
~Singleton()
{
std::cout << "~singleton()\n";
}
// コピー/ムーブ禁止
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
};
// これをヘッダに書いて複数のソースから#includeすると多重定義エラーになるので注意
Singleton& Singleton::sInstance = Singleton::getInstance();
int main()
{
std::cout << "start of main()\n";
auto& aSingleton0 = Singleton::getInstance();
std::cout << "aSingleton0 : " << &aSingleton0 << "\n";
auto& aSingleton1 = Singleton::getInstance();
std::cout << "aSingleton1 : " << &aSingleton1 << "\n";
std::cout << "end of main()\n";
}
ムーブもメンバ変数自体はコピーすることが一般的です(移動するのは所有権のみ)ので、この例ではムーブも禁止しました。
singleton() start of main() aSingleton0 : 0x601941 aSingleton1 : 0x601941 end of main() ~singleton()
main()関数の起動前にシングルトンがコンストラクトされ、main()関数終了後にシングルトンがデストラクトされていることが分かります。
サブ・スレッドを起動するまでは当たり前ですがシングル・スレッド動作します。main()関数の起動前にサブ・スレッドを起動しなければ(普通はしないでしょう。する時は要注意)、シングルトンの生成がシングル・スレッド環境で実行されるので安心です。
2-3-2.シングルトンの量産にCRTPを使う
基本的には上記のSingletonクラスを、派生クラスを受け取るクラス・テンプレートとします。
template<class tDerived>
class Singleton
{
static tDerived& sInstance;
static void use(tDerived const&) { }
protected:
// コンストラクタをprotectedに定義する
Singleton()
{
std::cout << TYPENAME(tDerived) << "()\n";
}
public:
~Singleton()
{
std::cout << "~" << TYPENAME(tDerived) << "()\n";
}
// コピー/ムーブ禁止
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static tDerived& getInstance()
{
static tDerived instance;
use(sInstance);
return instance;
}
};
// テンプレートのODR例外により、これをヘッダに書いてもOK
template<class tDerived>
tDerived& Singleton<tDerived>::sInstance = Singleton<tDerived>::getInstance();
staticメンバ変数である sInstance の定義に派生クラスの型を使っています。これにより、基底クラスの中に、派生クラスの参照を保持しています。不思議な感じがします。
ところで、通常のクラスの場合、sInstanceはアクセスされなくても実体化されますが、クラス・テンプレートの場合は使われていないメンバは実体化されません。そこでsInstanceを実体化させるためにuse()関数で形だけ使っています。(この構造は、boostのserializationにあるsingleton.hppを参考にしています。)
安易な例で申し訳ないのですが、次のようにしてシングルトンを量産できます。
// 量産シングルトンA
class MassSingletonA : public Singleton<MassSingletonA>
{
friend class Singleton<MassSingletonA>;
int mData;
MassSingletonA() : mData(12345) { }
public:
void print(char const* iName)
{
std::cout << iName << ".print() mData = " << mData << " (" << &mData << ")\n";
}
};
// 量産シングルトンB
class MassSingletonB : public Singleton<MassSingletonB>
{
friend class Singleton<MassSingletonB>;
std::string mData;
MassSingletonB() : mData("default") { }
public:
void print(char const* iName)
{
std::cout << iName << ".print() mData = " << mData << " (" << &mData << ")\n";
}
};
MassSingletonA/Bをシングルトンとするためにはコンストラクタをprivateにする必要があります。
そして、基底クラスのSingleton
早速、試してみましょう。
int main()
{
std::cout << "start of main()\n";
auto& aMassSingletonA0 = MassSingletonA::getInstance();
aMassSingletonA0.print("aMassSingletonA0");
auto& aMassSingletonA1 = MassSingletonA::getInstance();
aMassSingletonA1.print("aMassSingletonA1");
auto& aMassSingletonB0 = MassSingletonB::getInstance();
aMassSingletonB0.print("aMassSingletonB0");
auto& aMassSingletonB1 = MassSingletonB::getInstance();
aMassSingletonB1.print("aMassSingletonB1");
std::cout << "end of main()\n";
}
MassSingletonA() MassSingletonB() start of main() aMassSingletonA0.print() mData = 12345 (0x6026ec) aMassSingletonA1.print() mData = 12345 (0x6026ec) aMassSingletonB0.print() mData = default (0x602700) aMassSingletonB1.print() mData = default (0x602700) end of main() ~MassSingletonB() ~MassSingletonA()
ちゃんとインスタンスは1つだけ生成されているようです。
Wandboxで確認する。
#if 0を#if 1へ片方づつ変更してみて下さい。生成、コピー、ムーブどれをやってもコンパイル・エラーになります。
3.まとめ
CRTP如何でしたか? 普通は基底クラスから派生クラスをアクセスできないので不思議な使い方ですよね。
さて、仮想関数を使うと基底クラス経由で派生クラスの機能を使えるため似た使い方ができます。仮想関数なら動的ポリモーフィズムにも使えるので強力です。しかし、僅かですがオーバーヘッドがあります。またシングルトンの量産のような使い方はは難しいでしょう。
このように、一長一短ありますので適材適所でお使い下さい。
さて、来週は所要でお休みさせて頂きます。その次はもう3月ですね。時が経つのは速いものです。
まだ次回のネタを決めることができていませんが、3月までには何か決めて解説します。乞うご期待!
