テンプレートテンプレートパラメータの用途は何ですか?


238

テンプレートベースのパラメーター(テンプレートをパラメーターとして使用するテンプレート)を使用してポリシーベースのクラス設計を行うC ++の例をいくつか見ました。この手法には他にどのような用途がありますか?


4
私は他の方向(FP、Haskellなど)から来て、これに着陸しました:stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Erik Kaplun

回答:


197

次のように、テンプレートテンプレートの構文を使用して、タイプが別のテンプレートに依存するテンプレートであるパラメーターを渡す必要があると思います。

template <template<class> class H, class S>
void f(const H<S> &value) {
}

これHはテンプレートですが、この関数でのすべての特殊化を処理する必要がありましたH

:私は長年c ++をプログラミングしており、これが必要になったのは1回だけです。私はそれがめったに必要とされない機能であることがわかります(もちろんそれが必要なときに便利です!)。

私は良い例を考え、正直に言うと、ほとんどの場合これは必要ありませんが、例を考えてみましょう。のは、それはふりをしてみましょうstd::vector しない持っていますtypedef value_type

それでは、vectors要素に適切な型の変数を作成できる関数をどのように記述しますか?これはうまくいくでしょう。

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;
}

:にstd::vectorは、タイプとアロケータの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!

更新:この不自然な例でさえ、例証ではありますが、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;
}

これが、このタイプのコードを作成する方法です。


1
fがライブラリのユーザーによって定義された関数である場合、ユーザーが引数としてstd :: allocator <T>を渡す必要があるのは醜いです。std :: allocator引数のないバージョンは、std :: vectorのデフォルトパラメータを使用して機能することを期待していました。このwrt C ++ 0xに更新はありますか?
2011年

まあ、あなたはアロケータを提供する必要はありません。重要なのは、テンプレートテンプレートパラメータが正しい数の引数で定義されていることです。:しかし、機能は彼らの「種類」や意味、C ++ 98にも作品を以下の何気にはならないtemplate<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

なぜインスタンス化がそうでf<vector,int>ありそうでないのかf<vector<int>>
bobobobo 2013

2
@boboboboこれら2つの意味は異なります。f<vector,int>意味するf<ATemplate,AType>f<vector<int>>意味するf<AType>
user362515 2014年

@phaedrus:(かなり後で...)良い点、アロケータを汎用にする例を改善し、例をより明確にする:-)
Evan Teran

163

実際、テンプレートテンプレートパラメータのユースケースはかなり明白です。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;
}

次に、vectorのコードはまったく同じで、forward_listも同じであることがわかります。実際には、多数のマップタイプでも同じです。これらのテンプレートクラスには、メタインターフェース/プロトコル以外の共通点はありません。テンプレートテンプレートパラメーターを使用すると、それらすべての共通点をキャプチャできます。ただし、テンプレートの作成に進む前に、参照を確認して、シーケンスコンテナーが値の型とアロケーターの2つのテンプレート引数を受け入れることを思い出してください。アロケーターはデフォルトで設定されていますが、テンプレートオペレーターでの存在を考慮する必要があります<<:

template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...

これは、標準プロトコルに準拠している現在および将来のすべてのシーケンスコンテナーに対して自動的に機能します。ミックスにマップを追加するには、4つのテンプレートパラメーターを受け入れることに注意するために参照をのぞいてみる必要があるため、4引数のテンプレートテンプレートパラメーターを使用した上記のoperator <<の別のバージョンが必要になります。std:pairは、以前に定義したシーケンスタイプに対して2引数の演算子<<でレンダリングされることもわかるので、std :: pairだけに特化したものを提供します。

ところで、可変テンプレートを許可する(したがって可変テンプレートテンプレート引数を許可する)C + 11を使用すると、単一の演算子<<ですべてをルール化することが可能になります。例えば:

#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 

9
これは、誰もが対処しなければならないケースを示しているため、テンプレートテンプレートパラメータのとても良い例です。
Ravenwater 2013

3
これは、C ++テンプレートで私にとって最も目覚める答えです。@WhozCraigテンプレート拡張の詳細はどのようにして入手しましたか?
2014

3
@Arun gccはと呼ばれるマクロをサポートし__PRETTY_FUNCTION__ます。これは、特に、テンプレートパラメータの説明をプレーンテキストで報告します。clangもそれを行います。時々最も便利な機能です(ご覧のとおり)。
WhozCraig

19
ここでのテンプレートテンプレートパラメーターは、実際には値を追加していません。クラステンプレートの特定のインスタンスとして、通常のテンプレートパラメータを使用することもできます。
David Stone、

9
デビッドストーンに同意する必要があります。ここでは、テンプレートテンプレートパラメータに意味はありません。プレーンなテンプレート(テンプレート<typename Container>)を作成することは、はるかに単純で同等に効果的です。私はこの投稿がかなり古いことを知っているので、テンプレートテンプレートに関する情報を探してこの回答を見つけた人にのみ2セントを追加します。
ジム

67

Andrei Alexandrescuによる「Modern C ++ Design-Generic Programming and Design Patterns Applied」から簡単な例を以下に示します

彼はポリシーパターンを実装するために、テンプレートテンプレートパラメータを持つクラスを使用します。

// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
   ...
};

彼は説明します: 通常、ホストクラスはポリシークラスのテンプレート引数をすでに知っているか、簡単に推測できます。上記の例では、WidgetManagerは常にWidgetタイプのオブジェクトを管理するため、CreationPolicyのインスタンス化でユーザーにWidgetの再指定を要求することは冗長であり、潜在的に危険です。この場合、ライブラリコードはテンプレートテンプレートパラメーターを使用してポリシーを指定できます。

その効果は、クライアントコードが「WidgetManager」をよりエレガントな方法で使用できることです。

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

テンプレートテンプレート引数のない定義が必要とする、より面倒でエラーが発生しやすい方法の代わりに、

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;

1
ポリシーパターン以外の例について具体的に要求された質問。
user2913094 2015

私はまさにこの本からこの質問に来ました。注目に値するのは、テンプレートテンプレートパラメータがTypelistの章とTypelistsによるクラスの生成の章にも記載されていることです。
ビクター

18

これが私の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になります。


「テンプレート<クラスT、テンプレート<T> TT> CLayerT」や「クラスCLayerCuda:public CLayerT <TensorGPU <float >>」のようなものでTの推論を強制できますか?TT <otherT>が必要ない場合
NicoBerrogorry

決して気にしないでください:template <template <class T> class U> class B1 {}; ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/...簡単にGoogle検索から
NicoBerrogorry

12

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;

派生テンプレートに他のテンプレートパラメーターを直接指定する必要がないことに注意してください。「インターフェース」はまだそれらを受け取ります。

これにより、派生テンプレートからアクセスできる型パラメーターに依存する「インターフェイス」で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が必要です。


私はいくつかのコードにこの正確なソリューションが必要でした(ありがとう!)。それは機能しderivedますが、テンプレート引数なしでテンプレートクラスをどのように使用できるかわかりません。つまり、行typedef typename interface<derived, VALUE> type;
Carlton

@Carltonは、対応するテンプレートパラメータがとして定義されているため、基本的に機能しますtemplate <typename>。ある意味では、テンプレートパラメータは「メタタイプ」を持つと考えることができます。テンプレートパラメータの通常のメタタイプは、通常のタイプでtypename埋める必要があることを意味します。templateメタタイプは、それがテンプレートを参照して充填する必要があることを意味します。 derivedは、1つのtypenameメタタイプパラメータを受け入れるテンプレートを定義するため、法案に適合し、ここで参照できます。理にかなっていますか?
マークマッケナ2018年

C ++ 11はまだtypedefです。また、DERIVED型intなどの標準構成を使用することにより、最初の例での重複を回避できますvalue_type
rubenvb

この回答は、実際にはC ++ 11を対象としていません。私はあなたがtypedefブロック2から問題を回避できると言ってC ++ 11を参照しましたが、ポイント2は有効だと思います...ええ、それはおそらく同じことを行うためのより簡単な方法でしょう。
マークマッケナ

7

これは私が遭遇したものです:

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();
}

4

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;
}

2

これは、私が使用したものから一般化したものです。これは非常に単純な例であり、デフォルトの引数とともに実際の使用例を示しているため、ここに投稿します。

#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;
};

2

コードの可読性が向上し、型の安全性が向上し、コンパイラーの労力が一部節約されます。

コンテナーの各要素を印刷する場合は、テンプレートテンプレートパラメーターなしで次のコードを使用できます。

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';
}

整数sayを渡すと仮定しますprint_container(3)。前者の場合、cforループでの使用について文句を言うコンパイラーによってテンプレートがインスタンス化され、一致する型が見つからないため、後者はテンプレートをまったくインスタンス化しません。

一般的に、テンプレートクラス/関数がテンプレートクラスとしてテンプレートクラスを処理するように設計されている場合は、それを明確にすることをお勧めします。


1

バージョン付きのタイプに使用します。

などのテンプレートでバージョン管理されているタイプMyType<version>がある場合は、バージョン番号を取得できる関数を作成できます。

template<template<uint8_t> T, uint8_t Version>
Foo(const T<Version>& obj)
{
    assert(Version > 2 && "Versions older than 2 are no longer handled");
    ...
    switch (Version)
    {
    ...
    }
}

そのため、型ごとにオーバーロードを設定する代わりに、渡される型のバージョンに応じてさまざまなことを実行できます。一般的な方法でを取り込んでMyType<Version>返す変換関数を使用することもできます。また、古いバージョンからタイプの最新バージョンを返す関数をMyType<Version+1>再帰することもできToNewest()ます(しばらく前に保存された可能性のあるログに非常に役立ちます)ただし、今日の最新ツールで処理する必要があります)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.