Apa saja kegunaan parameter templat templat?

Jawaban:

197

Saya pikir Anda perlu menggunakan sintaks templat templat untuk lulus parameter yang tipenya templat bergantung pada templat lain seperti ini:

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

Di sini, Hadalah templat, tapi saya ingin fungsi ini menangani semua spesialisasi H.

CATATAN : Saya telah memprogram c ++ selama bertahun-tahun dan hanya membutuhkan ini sekali. Saya menemukan bahwa ini adalah fitur yang jarang diperlukan (tentu saja berguna ketika Anda membutuhkannya!).

Saya sudah mencoba memikirkan contoh yang baik, dan jujur, sebagian besar waktu ini tidak diperlukan, tetapi mari kita buat contoh. Mari kita berpura-pura std::vector tidak punya typedef value_type.

Jadi bagaimana Anda menulis fungsi yang dapat membuat variabel dari tipe yang tepat untuk elemen vektor? Ini akan berhasil.

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

CATATAN : std::vectormemiliki dua parameter templat, jenis, dan pengalokasi, jadi kami harus menerima keduanya. Untungnya, karena pengurangan tipe, kita tidak perlu menuliskan tipe yang tepat secara eksplisit.

yang dapat Anda gunakan seperti ini:

f<std::vector, int>(v); // v is of type std::vector<int> using any allocator

atau lebih baik lagi, kita bisa menggunakan:

f(v); // everything is deduced, f can deal with a vector of any type!

PEMBARUAN : Bahkan contoh yang dibuat-buat ini, walaupun bersifat ilustratif, bukan lagi contoh yang luar biasa karena pengenalan c ++ 11 auto. Sekarang fungsi yang sama dapat ditulis sebagai:

template <class Cont>
void f(Cont &v) {

    auto temp = v.back();
    v.pop_back();
    // Do some work on temp

    std::cout << temp << std::endl;
}

begitulah cara saya lebih suka menulis kode jenis ini.

Evan Teran
sumber
1
Jika f adalah fungsi yang didefinisikan oleh pengguna perpustakaan, itu jelek bahwa pengguna harus lulus std :: dialokasikan <T> sebagai argumen. Saya akan berharap bahwa versi tanpa argumen std :: pengalokasi telah bekerja menggunakan parameter default std :: vector. Apakah ada pembaruan pada wrt C ++ 0x ini?
amit
Nah, Anda tidak harus menyediakan pengalokasi. Yang penting adalah bahwa parameter templat templat ditentukan atas jumlah argumen yang benar. Tetapi fungsi tersebut seharusnya tidak peduli apa "tipe" atau artinya, mengikuti berfungsi dengan baik di C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon
Saya bertanya-tanya mengapa instantiasi adalah f<vector,int>dan tidak f<vector<int>>.
bobobobo
2
@obobobo Kedua hal ini memiliki arti yang berbeda. f<vector,int>berarti f<ATemplate,AType>, f<vector<int>>berartif<AType>
user362515
@phaedrus: (lama kemudian ...) poin yang bagus, meningkatkan contoh untuk menjadikan pengalokasi generik dan contoh lebih jelas :-)
Evan Teran
163

Sebenarnya, usecase untuk parameter templat templat agak jelas. Setelah Anda mengetahui bahwa C ++ stdlib memiliki lubang menganga tidak mendefinisikan operator keluaran aliran untuk jenis kontainer standar, Anda akan melanjutkan untuk menulis sesuatu seperti:

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

Maka Anda akan mengetahui bahwa kode untuk vektor adalah sama, untuk forward_list adalah sama, sebenarnya, bahkan untuk banyak jenis peta itu masih sama. Kelas-kelas template itu tidak memiliki kesamaan kecuali untuk meta-interface / protokol, dan menggunakan parameter template template memungkinkan untuk menangkap kesamaan di semua dari mereka. Sebelum melanjutkan untuk menulis templat, ada baiknya memeriksa referensi untuk mengingat bahwa wadah urutan menerima 2 argumen templat - untuk jenis nilai dan pengalokasi. Meskipun pengalokasi default, kita masih harus memperhitungkan keberadaannya di operator template kami <<:

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

Voila, yang akan bekerja secara otomatis untuk semua kontainer urutan sekarang dan masa depan mematuhi protokol standar. Untuk menambahkan peta ke dalam campuran, perlu mengintip referensi untuk mencatat bahwa mereka menerima 4 params templat, jadi kita membutuhkan versi lain dari operator << di atas dengan param templat templat 4-arg. Kami juga akan melihat bahwa std: pair mencoba dirender dengan operator 2-arg << untuk tipe urutan yang kami tentukan sebelumnya, jadi kami akan menyediakan spesialisasi hanya untuk std :: pair.

Btw, dengan C + 11 yang memungkinkan templat variadic (dan karenanya seharusnya mengizinkan templat templat variadic), mungkin saja memiliki operator tunggal << untuk mengatur semuanya. Sebagai contoh:

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

Keluaran

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 
pfalcon
sumber
9
Ini adalah contoh manis dari parameter templat templat, karena menunjukkan kasus yang harus dihadapi semua orang.
Ravenwater
3
Ini adalah jawaban paling membangkitkan bagi saya dalam template C ++. @WhozCraig Bagaimana Anda mendapatkan detail perluasan template?
Arun
3
@Arun gcc mendukung makro yang disebut __PRETTY_FUNCTION__, yang, antara lain, melaporkan deskripsi parameter templat dalam teks biasa. dentang melakukannya juga. Fitur yang paling berguna kadang-kadang (seperti yang Anda lihat).
WhozCraig
20
Parameter templat templat di sini tidak benar-benar menambahkan nilai apa pun. Anda mungkin juga hanya menggunakan parameter templat biasa sebagai contoh yang diberikan templat kelas.
David Stone
9
Saya harus setuju dengan David Stone. Tidak ada titik ke parameter templat templat di sini. Akan jauh lebih sederhana dan sama efektifnya untuk membuat templat biasa (templat <typename Container>). Saya tahu posting ini cukup lama, jadi saya hanya menambahkan 2 sen untuk orang yang menemukan jawaban ini mencari info tentang template templat.
Jim Vargo
67

Berikut adalah contoh sederhana yang diambil dari 'Desain C ++ Modern - Pemrograman Generik dan Pola Desain yang Diterapkan' oleh Andrei Alexandrescu:

Dia menggunakan kelas dengan parameter templat templat untuk menerapkan pola kebijakan:

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

Dia menjelaskan: Biasanya, kelas host sudah tahu, atau dapat dengan mudah menyimpulkan, argumen templat dari kelas kebijakan. Dalam contoh di atas, WidgetManager selalu mengelola objek dari jenis Widget, sehingga mengharuskan pengguna untuk menentukan Widget lagi di Instansiasi dari CreationPolicy adalah berlebihan dan berpotensi berbahaya. Dalam hal ini, kode perpustakaan dapat menggunakan parameter templat templat untuk menentukan kebijakan.

Efeknya adalah bahwa kode klien dapat menggunakan 'WidgetManager' dengan cara yang lebih elegan:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Alih-alih cara yang lebih rumit, dan cenderung rawan bahwa definisi yang membutuhkan argumen template template:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
yoav.aviram
sumber
1
Pertanyaan itu secara spesifik meminta contoh selain pola kebijakan.
user2913094
Saya sampai pada pertanyaan ini tepatnya dari buku ini. Catatan yang layak adalah bahwa parameter templat templat juga muncul di bab Pengetik dan pembuatan kelas dengan bab Pengetik .
Victor
18

Berikut adalah contoh praktis lain dari perpustakaan jaringan saraf CUDA Convolutional saya . Saya memiliki templat kelas berikut:

template <class T> class Tensor

yang sebenarnya mengimplementasikan manipulasi matriks n-dimensi. Ada juga templat kelas anak:

template <class T> class TensorGPU : public Tensor<T>

yang mengimplementasikan fungsi yang sama tetapi dalam GPU. Kedua templat dapat bekerja dengan semua tipe dasar, seperti float, double, int, dll. Dan saya juga memiliki templat kelas (disederhanakan):

template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
    TT<T> weights;
    TT<T> inputs;
    TT<int> connection_matrix;
}

Alasan di sini untuk memiliki sintaks template templat adalah karena saya dapat mendeklarasikan implementasi kelas

class CLayerCuda: public CLayerT<TensorGPU, float>

yang akan memiliki bobot dan input tipe float dan pada GPU, tetapi connection_matrix akan selalu int, baik pada CPU (dengan menentukan TT = Tensor) atau pada GPU (dengan menentukan TT = TensorGPU).

Mikhail Sirotenko
sumber
Bisakah Anda memaksa pengurangan T dengan sesuatu seperti: "template <class T, template <T> TT> CLayerT" dan "class CLayerCuda: public CLayerT <TensorGPU <float>>"? Jika Anda tidak membutuhkan TT <otherT>
NicoBerrogorry
PERNAH PIKIRAN: template <template <class T> class U> class B1 {}; dari ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/… dari pencarian google cepat
NicoBerrogorry
12

Katakanlah Anda menggunakan CRTP untuk menyediakan "antarmuka" untuk satu set templat anak; dan kedua orang tua dan anak adalah parametrik dalam argumen templat lain:

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;

Perhatikan duplikasi 'int', yang sebenarnya merupakan parameter tipe yang sama yang ditentukan untuk kedua templat. Anda dapat menggunakan templat templat untuk DERIVED untuk menghindari duplikasi ini:

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;

Perhatikan bahwa Anda menghilangkan secara langsung menyediakan parameter templat lain ke turunan templat ; "antarmuka" masih menerimanya.

Ini juga memungkinkan Anda membangun typedef di "antarmuka" yang bergantung pada parameter tipe, yang akan dapat diakses dari templat yang diturunkan.

Typedef di atas tidak berfungsi karena Anda tidak dapat mengetik ke template yang tidak ditentukan. Ini berfungsi, namun (dan C ++ 11 memiliki dukungan asli untuk typedefs templat):

template <typename VALUE>
struct derived_interface_type {
    typedef typename interface<derived, VALUE> type;
};

typedef typename derived_interface_type<int>::type derived_t;

Sayangnya, Anda memerlukan satu turunan_interface_type untuk setiap contoh kerangka turunan, kecuali ada trik lain yang belum saya pelajari.

Mark McKenna
sumber
Saya membutuhkan solusi tepat ini untuk beberapa kode (terima kasih!). Meskipun berfungsi, saya tidak mengerti bagaimana kelas templat deriveddapat digunakan tanpa argumen templatnya, yaitu baristypedef typename interface<derived, VALUE> type;
Carlton
@Carlton pada dasarnya berfungsi karena parameter template yang sesuai sedang diisi didefinisikan sebagai a template <typename>. Dalam arti tertentu, Anda dapat menganggap parameter template memiliki 'metatype'; metatype normal untuk parameter templat adalah typenameyang artinya harus diisi dengan tipe biasa; yang templateberarti metatype perlu diisi dengan referensi ke template. derivedmendefinisikan templat yang menerima satu typenameparameter metatyped, sehingga sesuai dengan tagihan dan dapat dirujuk di sini. Masuk akal?
Mark McKenna
C ++ 11 masih typedef. Selain itu, Anda dapat menghindari duplikat intdalam contoh pertama Anda dengan menggunakan konstruk standar seperti value_typepada tipe DERIVED.
rubenvb
Jawaban ini sebenarnya tidak menargetkan C ++ 11; Saya mereferensikan C ++ 11 hanya untuk mengatakan Anda dapat mengatasi typedefmasalah dari blok 2. Tapi poin 2 valid saya pikir ... ya, itu mungkin akan menjadi cara yang lebih sederhana untuk melakukan hal yang sama.
Mark McKenna
7

Inilah yang saya temui:

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)>>>>>>>>
{

};

Dapat diatasi untuk:

template<class A>
class B
{
  A& a;
};

template< template<class> class B>
class A
{
  B<A> b;
};

class AInstance : A<B> //happy
{

};

atau (kode kerja):

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();
}
Kue kering
sumber
4

Dalam solusi dengan templat variadic yang disediakan oleh pfalcon, saya merasa kesulitan untuk benar-benar mengkhususkan operator ostream untuk std :: map karena sifat serakah dari spesialisasi variadic. Inilah sedikit revisi yang berhasil bagi saya:

#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;
}
Kuberan Naganathan
sumber
2

Ini salah satu yang digeneralisasi dari sesuatu yang baru saja saya gunakan. Saya mempostingnya karena ini adalah contoh yang sangat sederhana, dan menunjukkan penggunaan praktis bersama dengan argumen default:

#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;
};
Imallett
sumber
2

Ini meningkatkan keterbacaan kode Anda, memberikan keamanan tipe tambahan dan menghemat beberapa upaya kompiler.

Katakanlah Anda ingin mencetak setiap elemen wadah, Anda dapat menggunakan kode berikut tanpa parameter templat templat

template <typename T> void print_container(const T& c)
{
    for (const auto& v : c)
    {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

atau dengan parameter templat templat

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

Misalkan Anda memasukkan bilangan bulat, katakanlah print_container(3). Untuk kasus sebelumnya, template akan dipakai oleh kompiler yang akan mengeluh tentang penggunaanc dalam for for, yang terakhir tidak akan instantiate templat sama sekali karena tidak ada jenis pencocokan yang dapat ditemukan.

Secara umum, jika kelas / fungsi templat Anda dirancang untuk menangani kelas templat sebagai parameter templat, lebih baik untuk membuatnya jelas.

colin
sumber
1

Saya menggunakannya untuk tipe berversi.

Jika Anda memiliki tipe yang diversi melalui templat seperti MyType<version>, Anda bisa menulis fungsi yang bisa Anda ambil nomor versinya:

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)
    {
    ...
    }
}

Jadi Anda dapat melakukan hal-hal yang berbeda tergantung pada versi dari jenis yang diteruskan alih-alih memiliki kelebihan untuk masing-masing jenis. Anda juga dapat memiliki fungsi konversi yang menerima MyType<Version>dan mengembalikan MyType<Version+1>, dengan cara yang umum, dan bahkan mengembalikannya untuk memiliki ToNewest()fungsi yang mengembalikan versi jenis terbaru dari versi yang lebih lama (sangat berguna untuk log yang mungkin telah disimpan beberapa waktu yang lalu) tetapi perlu diproses dengan alat terbaru hari ini).

cd127
sumber