ポリシーベースのクラス設計を行うために、テンプレートテンプレートパラメーター(テンプレートをパラメーターとして取るテンプレート)を使用したC++の例をいくつか見てきました。この手法には他にどのような用途がありますか?
テンプレートテンプレートの構文を使用して、型が次のような別のテンプレートに依存するテンプレートであるパラメーターを渡す必要があると思います。
template <template<class> class H, class S>
void f(const H<S> &value) {
}
ここでは、H
はテンプレートですが、この関数がH
のすべての特殊化を処理することを望んでいました。
NOTE:私は長年c ++をプログラミングしてきましたが、これは一度しか必要ありません。私はそれがめったに必要とされない機能であることがわかります(もちろん必要なときに便利です!)。
私は良い例について考えようとしており、正直に言うと、ほとんどの場合これは必要ではありませんが、例を考えてみましょう。 std::vector
がtypedef value_type
を持たないとします。
それでは、ベクトル要素に適切な型の変数を作成できる関数をどのように作成しますか?これは動作します。
template <template<class, class> class V, class T, class A>
void f(V<T, A> &v) {
// This can be "typename V<T, A>::value_type",
// but we are pretending we don't have it
T temp = v.back();
v.pop_back();
// Do some work on temp
std::cout << temp << std::endl;
}
NOTE:we std::vector
には、typeとallocatorの2つのテンプレートパラメーターがあるため、両方を受け入れる必要がありました。幸いなことに、型の推定のため、正確な型を明示的に記述する必要はありません。
次のように使用できます:
f<std::vector, int>(v); // v is of type std::vector<int> using any allocator
それ以上の場合、次のように使用できます。
f(v); // everything is deduced, f can deal with a vector of any type!
UPDATE:この不自然な例でさえ、例証ではありますが、c ++ 11がauto
を導入するため、もはや驚くべき例ではありません。同じ関数を次のように書くことができます:
template <class Cont>
void f(Cont &v) {
auto temp = v.back();
v.pop_back();
// Do some work on temp
std::cout << temp << std::endl;
}
これが、このタイプのコードの作成方法です。
実際、テンプレートテンプレートパラメータのユースケースはかなり明白です。 C++ stdlibに標準のコンテナタイプのストリーム出力演算子を定義しないという大きな穴があることがわかったら、次のように記述します。
template<typename T>
static inline std::ostream& operator<<(std::ostream& out, std::list<T> const& v)
{
out << '[';
if (!v.empty()) {
for (typename std::list<T>::const_iterator i = v.begin(); ;) {
out << *i;
if (++i == v.end())
break;
out << ", ";
}
}
out << ']';
return out;
}
次に、ベクターのコードがまったく同じであることがわかります。forward_listが同じであるため、実際には、多数のマップタイプでも同じです。これらのテンプレートクラスには、メタインターフェイス/プロトコル以外の共通点はありません。テンプレートテンプレートパラメータを使用すると、それらすべての共通性をキャプチャできます。ただし、テンプレートの作成に進む前に、参照を確認して、シーケンスコンテナーが値の型とアロケーターの2つのテンプレート引数を受け入れることを思い出してください。アロケーターはデフォルトになっていますが、テンプレートoperator <<での存在を考慮する必要があります。
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
これは、標準プロトコルに準拠している現在および将来のすべてのシーケンスコンテナーに対して自動的に機能します。ミックスにマップを追加するには、4つのテンプレートパラメーターを受け入れることを確認するために参照を参照する必要があるため、4-argテンプレートテンプレートパラメーターを持つ上記のoperator <<の別のバージョンが必要になります。また、前に定義したシーケンス型に対してstd:pairが2-arg operator <<でレンダリングされようとするので、std :: pairだけに特殊化を提供します。
ところで、可変長テンプレートを許可する(したがって可変長テンプレートテンプレート引数を許可する)C + 11を使用すると、単一のoperator <<を使用してすべてを支配することができます。例えば:
#include <iostream>
#include <vector>
#include <deque>
#include <list>
template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs)
{
os << __PRETTY_FUNCTION__ << '\n';
for (auto const& obj : objs)
os << obj << ' ';
return os;
}
int main()
{
std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 };
std::cout << vf << '\n';
std::list<char> lc { 'a', 'b', 'c', 'd' };
std::cout << lc << '\n';
std::deque<int> di { 1, 2, 3, 4 };
std::cout << di << '\n';
return 0;
}
出力
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = float, C = vector, Args = <std::__1::allocator<float>>]
1.1 2.2 3.3 4.4
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = char, C = list, Args = <std::__1::allocator<char>>]
a b c d
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = int, C = deque, Args = <std::__1::allocator<int>>]
1 2 3 4
Andrei Alexandrescuによる 'Modern C++ Design-Generic Programming and Design Patterns Applied' から抜粋した簡単な例を次に示します。
彼は、ポリシーパターンを実装するために、テンプレートテンプレートパラメータを持つクラスを使用します。
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
彼は説明します:通常、Hostクラスはポリシークラスのテンプレート引数を既に知っているか、簡単に推測できます。上記の例では、WidgetManagerは常にWidgetタイプのオブジェクトを管理するため、CreationPolicyのインスタンス化でユーザーにWidgetを再度指定することは冗長であり、潜在的に危険です。この場合、ライブラリコードはポリシーを指定するためにテンプレートテンプレートパラメーターを使用できます。
その効果は、クライアントコードが「WidgetManager」をよりエレガントな方法で使用できることです。
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
テンプレートテンプレート引数を欠いた定義が必要とする、より面倒でエラーを起こしやすい方法の代わりに:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
CUDA畳み込みニューラルネットワークライブラリ の別の実用的な例を次に示します。次のクラステンプレートがあります。
template <class T> class Tensor
これは実際にn次元の行列操作を実装しています。子クラステンプレートもあります。
template <class T> class TensorGPU : public Tensor<T>
gPUで同じ機能を実装します。両方のテンプレートは、float、double、intなどのすべての基本型で機能します。また、クラステンプレート(簡略化)もあります。
template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
TT<T> weights;
TT<T> inputs;
TT<int> connection_matrix;
}
ここでテンプレートテンプレート構文を使用する理由は、クラスの実装を宣言できるためです。
class CLayerCuda: public CLayerT<TensorGPU, float>
これは、float型の入力とGPUの両方の重みを持ちますが、connection_matrixは、CPU(TT = Tensorを指定)またはGPU(TT = TensorGPUを指定)で常にintになります。
CRTPを使用して、一連の子テンプレートに「インターフェース」を提供するとします。そして、親と子の両方が他のテンプレート引数でパラメトリックです:
template <typename DERIVED, typename VALUE> class interface {
void do_something(VALUE v) {
static_cast<DERIVED*>(this)->do_something(v);
}
};
template <typename VALUE> class derived : public interface<derived, VALUE> {
void do_something(VALUE v) { ... }
};
typedef interface<derived<int>, int> derived_t;
'int'の重複に注意してください。これは、実際には両方のテンプレートに指定された同じ型パラメーターです。この重複を避けるために、DERIVEDのテンプレートテンプレートを使用できます。
template <template <typename> class DERIVED, typename VALUE> class interface {
void do_something(VALUE v) {
static_cast<DERIVED<VALUE>*>(this)->do_something(v);
}
};
template <typename VALUE> class derived : public interface<derived, VALUE> {
void do_something(VALUE v) { ... }
};
typedef interface<derived, int> derived_t;
derivedテンプレートに他のテンプレートパラメータを直接提供することを排除していることに注意してください。 「インターフェース」はまだそれらを受け取ります。
また、これにより、派生したテンプレートからアクセス可能な型パラメーターに依存する「インターフェイス」でtypedefを構築できます。
指定されていないテンプレートにtypedefできないため、上記のtypedefは機能しません。ただし、これは機能します(C++ 11にはテンプレートtypedefのネイティブサポートがあります)。
template <typename VALUE>
struct derived_interface_type {
typedef typename interface<derived, VALUE> type;
};
typedef typename derived_interface_type<int>::type derived_t;
残念ながら、派生テンプレートのインスタンス化ごとに1つのderived_interface_typeが必要です。残念ながら、まだ学んでいない別のトリックがない限りです。
これは私が遭遇したものです:
template<class A>
class B
{
A& a;
};
template<class B>
class A
{
B b;
};
class AInstance : A<B<A<B<A<B<A<B<... (oh oh)>>>>>>>>
{
};
以下に解決できます:
template<class A>
class B
{
A& a;
};
template< template<class> class B>
class A
{
B<A> b;
};
class AInstance : A<B> //happy
{
};
または(作業コード):
template<class A>
class B
{
public:
A* a;
int GetInt() { return a->dummy; }
};
template< template<class> class B>
class A
{
public:
A() : dummy(3) { b.a = this; }
B<A> b;
int dummy;
};
class AInstance : public A<B> //happy
{
public:
void Print() { std::cout << b.GetInt(); }
};
int main()
{
std::cout << "hello";
AInstance test;
test.Print();
}
Pfalconが提供する可変個引数テンプレートを使用したソリューションでは、可変個の特殊化の貪欲な性質のために、std :: mapのostream演算子を実際に特殊化するのは難しいことがわかりました。ここに私のために働いたわずかな改訂があります:
#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <map>
namespace containerdisplay
{
template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs)
{
std::cout << __PRETTY_FUNCTION__ << '\n';
for (auto const& obj : objs)
os << obj << ' ';
return os;
}
}
template< typename K, typename V>
std::ostream& operator << ( std::ostream& os,
const std::map< K, V > & objs )
{
std::cout << __PRETTY_FUNCTION__ << '\n';
for( auto& obj : objs )
{
os << obj.first << ": " << obj.second << std::endl;
}
return os;
}
int main()
{
{
using namespace containerdisplay;
std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 };
std::cout << vf << '\n';
std::list<char> lc { 'a', 'b', 'c', 'd' };
std::cout << lc << '\n';
std::deque<int> di { 1, 2, 3, 4 };
std::cout << di << '\n';
}
std::map< std::string, std::string > m1
{
{ "foo", "bar" },
{ "baz", "boo" }
};
std::cout << m1 << std::endl;
return 0;
}
これは、私が今使ったものから一般化したものです。 veryの簡単な例であるため、投稿していますが、デフォルトの引数とともに実用的な使用例を示しています。
#include <vector>
template <class T> class Alloc final { /*...*/ };
template <template <class T> class allocator=Alloc> class MyClass final {
public:
std::vector<short,allocator<short>> field0;
std::vector<float,allocator<float>> field1;
};
コードの可読性が向上し、型の安全性が向上し、コンパイラーの手間が省けます。
コンテナの各要素を印刷するとします。テンプレートテンプレートパラメータなしで次のコードを使用できます
template <typename T> void print_container(const T& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}
またはテンプレートテンプレートパラメーターを使用
template< template<typename, typename> class ContainerType, typename ValueType, typename AllocType>
void print_container(const ContainerType<ValueType, AllocType>& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}
print_container(3)
などの整数を渡すと仮定します。前者の場合、forループでのc
の使用について文句を言うコンパイラによってテンプレートがインスタンス化されます。後者は、一致するタイプが見つからないため、テンプレートをまったくインスタンス化しません。
一般的に、テンプレートクラス/関数がテンプレートパラメータとしてテンプレートクラスを処理するように設計されている場合は、明確にすることをお勧めします。