Một số sử dụng các tham số mẫu mẫu là gì?


238

Tôi đã thấy một số ví dụ về C ++ bằng cách sử dụng các tham số mẫu mẫu (đó là các mẫu lấy mẫu làm tham số) để thiết kế lớp dựa trên chính sách. Kỹ thuật này có những công dụng nào khác?


4
Tôi đến từ hướng khác (FP, Haskell, v.v.) và đã đáp xuống điều này: stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Erik Kaplun

Câu trả lời:


197

Tôi nghĩ rằng bạn cần sử dụng cú pháp mẫu mẫu để truyền tham số có loại là mẫu phụ thuộc vào mẫu khác như thế này:

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

Ở đây, Hlà một mẫu, nhưng tôi muốn chức năng này xử lý tất cả các chuyên ngành H.

LƯU Ý : Tôi đã lập trình c ++ trong nhiều năm và chỉ cần điều này một lần. Tôi thấy rằng đó là một tính năng hiếm khi cần thiết (tất nhiên tiện dụng khi bạn cần nó!).

Tôi đã cố gắng nghĩ về những ví dụ hay, và thành thật mà nói, hầu hết thời gian điều này là không cần thiết, nhưng hãy lấy một ví dụ. Hãy giả vờ rằng std::vector không có a typedef value_type.

Vì vậy, làm thế nào bạn sẽ viết một hàm có thể tạo các biến đúng loại cho các phần tử vectơ? Điều này sẽ làm việc.

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

LƯU Ý : std::vectorcó hai tham số mẫu, loại và cấp phát, vì vậy chúng tôi phải chấp nhận cả hai tham số. May mắn thay, vì loại trừ, chúng tôi sẽ không cần phải viết ra loại chính xác một cách rõ ràng.

mà bạn có thể sử dụng như thế này:

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

hoặc tốt hơn nữa, chúng ta chỉ có thể sử dụng:

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

CẬP NHẬT : Ngay cả ví dụ giả định này, trong khi minh họa, không còn là một ví dụ tuyệt vời do giới thiệu c ++ 11 auto. Bây giờ chức năng tương tự có thể được viết là:

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

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

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

đó là cách tôi muốn viết loại mã này.


1
Nếu f là một hàm được xác định bởi người dùng của thư viện, thì thật tệ khi người dùng cần truyền std :: allocator <T> làm đối số. Tôi đã dự kiến ​​rằng phiên bản không có đối số std :: allocator đã hoạt động bằng cách sử dụng tham số mặc định của std :: vector. Có bản cập nhật nào trên wrt C ++ 0x này không?
amit

Vâng, bạn không phải cung cấp phân bổ. Điều quan trọng là tham số mẫu mẫu được xác định theo số lượng đối số chính xác. Nhưng chức năng không nên quan tâm "loại" hay ý nghĩa của chúng là gì, sau đây hoạt động tốt trong C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Tôi tự hỏi tại sao khởi tạo là f<vector,int>và không f<vector<int>>.
bobobobo 17/12/13

2
@bobobobo Hai nghĩa này khác nhau. f<vector,int>phương tiện f<ATemplate,AType>, f<vector<int>>phương tiệnf<AType>
dùng362515

@phaedrus: (muộn hơn ...) những điểm tốt, đã cải thiện ví dụ để làm cho người cấp phát chung chung và ví dụ rõ ràng hơn :-)
Evan Teran

163

Trên thực tế, usecase cho các tham số mẫu mẫu là khá rõ ràng. Khi bạn biết rằng C ++ stdlib có lỗ hổng không xác định toán tử đầu ra luồng cho các loại vùng chứa tiêu chuẩn, bạn sẽ tiến hành viết một cái gì đó như:

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

Sau đó, bạn sẽ nhận ra rằng mã cho vectơ là như nhau, đối với Forward_list là giống nhau, thực tế, ngay cả đối với vô số loại bản đồ, nó vẫn giống nhau. Các lớp mẫu đó không có gì chung ngoại trừ giao diện / giao thức meta và sử dụng tham số mẫu khuôn mẫu cho phép nắm bắt điểm chung trong tất cả chúng. Trước khi tiếp tục viết một mẫu, bạn nên kiểm tra một tham chiếu để nhớ lại rằng các bộ chứa chuỗi chấp nhận 2 đối số mẫu - cho loại giá trị và cấp phát. Mặc dù cấp phát được mặc định, chúng ta vẫn nên tính đến sự tồn tại của nó trong toán tử mẫu <<:

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

Voila, sẽ hoạt động tự động cho tất cả các container trình tự hiện tại và tương lai tuân thủ giao thức chuẩn. Để thêm bản đồ vào hỗn hợp, sẽ cần xem qua tham chiếu để lưu ý rằng họ chấp nhận 4 tham số mẫu, vì vậy chúng tôi cần một phiên bản khác của toán tử << ở trên với tham số mẫu mẫu 4-arg. Chúng ta cũng sẽ thấy rằng std: cặp cố gắng được kết xuất bằng toán tử 2-arg << cho các loại trình tự chúng ta đã xác định trước đó, vì vậy chúng tôi sẽ cung cấp một chuyên môn hóa chỉ dành cho cặp std ::.

Btw, với C + 11 cho phép các mẫu matrixdic (và do đó sẽ cho phép mẫu khuôn mẫu matrixdic), có thể có toán tử đơn << để điều khiển tất cả. Ví dụ:

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

Đầu ra

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
Đây là một ví dụ ngọt ngào về các tham số mẫu mẫu, vì nó cho thấy một trường hợp mà mọi người đều phải giải quyết.
Ravenwater

3
Đây là câu trả lời thức tỉnh nhất đối với tôi trong các mẫu C ++. @WhozCraig Làm thế nào bạn có được các chi tiết mở rộng mẫu?
Arun

3
@Arun gcc hỗ trợ một macro được gọi __PRETTY_FUNCTION__, trong số những thứ khác, báo cáo các mô tả tham số mẫu trong văn bản thuần túy. clang làm điều đó là tốt. Một tính năng tiện dụng nhất đôi khi (như bạn có thể thấy).
WhozCraig

19
Tham số mẫu mẫu ở đây không thực sự thêm bất kỳ giá trị nào. Bạn cũng có thể chỉ sử dụng một tham số mẫu thông thường như bất kỳ trường hợp cụ thể nào của mẫu lớp.
David Stone

9
Tôi phải đồng ý với David Stone. Không có điểm nào cho tham số mẫu mẫu ở đây. Sẽ đơn giản hơn và hiệu quả không kém khi tạo một mẫu đơn giản (mẫu <typename Container>). Tôi biết bài đăng này khá cũ, vì vậy tôi chỉ thêm 2 xu của mình cho những người tình cờ tìm thấy câu trả lời này để tìm thông tin về các mẫu mẫu.
Jim Vargo

67

Dưới đây là một ví dụ đơn giản được lấy từ 'Thiết kế C ++ hiện đại - Các mẫu thiết kế và lập trình chung được áp dụng' bởi Andrei Alexandrescu:

Anh ta sử dụng một lớp với các tham số mẫu khuôn mẫu để thực hiện mẫu chính sách:

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

Ông giải thích: Thông thường, lớp máy chủ đã biết hoặc có thể dễ dàng suy ra, đối số khuôn mẫu của lớp chính sách. Trong ví dụ trên, WidgetManager luôn quản lý các đối tượng loại Widget, do đó, yêu cầu người dùng chỉ định lại Widget trong phần khởi tạo của CreationPolicy là dự phòng và có khả năng nguy hiểm. Trong trường hợp này, mã thư viện có thể sử dụng các tham số mẫu mẫu để chỉ định chính sách.

Hiệu quả là mã máy khách có thể sử dụng 'Trình quản lý widget' theo cách thanh lịch hơn:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Thay vì cách cồng kềnh và dễ bị lỗi hơn mà một định nghĩa thiếu đối số mẫu khuôn mẫu sẽ yêu cầu:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;

1
Câu hỏi được yêu cầu cụ thể cho các ví dụ khác với mẫu chính sách.
dùng2913094

Tôi đến câu hỏi này chính xác từ cuốn sách này. Một lưu ý đáng chú ý là các tham số mẫu mẫu cũng xuất hiện trong chương typelist và thế hệ Class với chương typelists .
Victor

18

Đây là một ví dụ thực tế khác từ thư viện mạng thần kinh CUDA Convolutional của tôi . Tôi có mẫu lớp sau:

template <class T> class Tensor

mà thực sự là thực hiện thao tác ma trận n chiều. Ngoài ra còn có một mẫu lớp con:

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

trong đó thực hiện các chức năng tương tự nhưng trong GPU. Cả hai mẫu có thể hoạt động với tất cả các loại cơ bản, như float, double, int, v.v. Và tôi cũng có một mẫu lớp (đơn giản hóa):

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

Lý do ở đây để có cú pháp mẫu khuôn mẫu là vì tôi có thể khai báo việc thực hiện lớp

class CLayerCuda: public CLayerT<TensorGPU, float>

sẽ có cả trọng lượng và đầu vào của kiểu float và trên GPU, nhưng Connection_matrix sẽ luôn là int, trên CPU (bằng cách chỉ định TT = Tensor) hoặc trên GPU (bằng cách chỉ định TT = TensorGPU).


Bạn có thể buộc khấu trừ T bằng một cái gì đó như: "template <class T, template <T> TT> CLayerT" và "class CLayerCuda: công khai CLayerT <TensorGPU <float >>"? Trong trường hợp bạn không cần TT <otherT>
NicoBerrog lỗi

KHÔNG BAO GIỜ MIND: mẫu <mẫu <lớp T> lớp U> lớp B1 {}; từ ibm.com/support/ledgeledgecenter/en/SSLTBW_2.3.0/ trên từ một tìm kiếm nhanh trên google
NicoBerrog lỗi

12

Giả sử bạn đang sử dụng CRTP để cung cấp "giao diện" cho một tập hợp các mẫu con; và cả cha và con đều là tham số trong (các) đối số khuôn mẫu khác:

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;

Lưu ý sự trùng lặp của 'int', đây thực sự là tham số cùng loại được chỉ định cho cả hai mẫu. Bạn có thể sử dụng một mẫu mẫu cho DERIVED để tránh sự trùng lặp này:

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;

Lưu ý rằng bạn đang loại bỏ trực tiếp việc cung cấp (các) tham số mẫu khác cho mẫu dẫn xuất ; "giao diện" vẫn nhận được chúng.

Điều này cũng cho phép bạn xây dựng typedefs trong "giao diện" phụ thuộc vào các tham số loại, có thể truy cập được từ mẫu dẫn xuất.

Typedef ở trên không hoạt động vì bạn không thể gõ vào mẫu không xác định. Tuy nhiên, điều này hoạt động (và C ++ 11 có hỗ trợ riêng cho typedefs mẫu):

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

typedef typename derived_interface_type<int>::type derived_t;

Thật không may, bạn cần một dẫn xuất_interface_type cho mỗi lần khởi tạo mẫu dẫn xuất, trừ khi có một mẹo khác tôi chưa học được.


Tôi cần giải pháp chính xác này cho một số mã (cảm ơn!). Mặc dù nó hoạt động, tôi không hiểu làm thế nào lớp mẫu derivedcó thể được sử dụng mà không có đối số mẫu của nó, tức là dòngtypedef typename interface<derived, VALUE> type;
Carlton

@Carlton nó hoạt động cơ bản vì tham số mẫu tương ứng được điền được xác định là a template <typename>. Theo một nghĩa nào đó, bạn có thể nghĩ về các tham số mẫu là có 'siêu dữ liệu'; siêu dữ liệu thông thường cho một tham số mẫu là typenameđiều đó có nghĩa là nó cần được điền bởi một loại thông thường; các templatephương tiện metatype nó cần phải được làm đầy với một tham chiếu đến một bản mẫu. derivedđịnh nghĩa một mẫu chấp nhận một typenametham số biến đổi, vì vậy nó phù hợp với hóa đơn và có thể được tham chiếu ở đây. Có lý?
Đánh dấu McKenna

C ++ 11 vẫn còn typedef. Ngoài ra, bạn có thể tránh trùng lặp inttrong ví dụ đầu tiên của mình bằng cách sử dụng cấu trúc tiêu chuẩn, chẳng hạn như value_typetrong kiểu DERIVED.
rubenvb

Câu trả lời này không thực sự nhắm mục tiêu C ++ 11; Tôi đã tham khảo C ++ 11 chỉ để nói rằng bạn có thể giải typedefquyết vấn đề từ khối 2. Nhưng điểm 2 là hợp lệ Tôi nghĩ ... vâng, đó có lẽ là một cách đơn giản hơn để làm điều tương tự.
Đánh dấu McKenna

7

Đây là những gì tôi gặp phải:

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

};

Có thể được giải quyết để:

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

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

class AInstance : A<B> //happy
{

};

hoặc (mã làm việc):

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

Trong giải pháp với các mẫu matrixdic được cung cấp bởi pfalcon, tôi thấy thật khó để chuyên môn hóa toán tử Ostream cho std :: map do tính chất tham lam của chuyên môn hóa từ khóa. Đây là một sửa đổi nhỏ làm việc cho tôi:

#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

Đây là một khái quát từ một cái gì đó tôi vừa sử dụng. Tôi đang đăng nó vì đây là một ví dụ rất đơn giản và nó cho thấy trường hợp sử dụng thực tế cùng với các đối số mặc định:

#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

Nó cải thiện khả năng đọc mã của bạn, cung cấp thêm loại an toàn và tiết kiệm một số nỗ lực của trình biên dịch.

Giả sử bạn muốn in từng phần tử của một container, bạn có thể sử dụng đoạn mã sau mà không cần tham số mẫu mẫu

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

hoặc với tham số mẫu mẫu

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

Giả sử bạn vượt qua trong một số nguyên nói print_container(3). Đối với trường hợp trước, mẫu sẽ được trình biên dịch khởi tạo, nó sẽ phàn nàn về việc sử dụng ctrong vòng lặp for, cái sau sẽ không khởi tạo mẫu hoàn toàn vì không thể tìm thấy loại phù hợp.

Nói chung, nếu lớp / hàm mẫu của bạn được thiết kế để xử lý lớp mẫu làm tham số mẫu, tốt hơn là làm cho nó rõ ràng.


1

Tôi sử dụng nó cho các loại phiên bản.

Nếu bạn có một loại được phiên bản thông qua một mẫu như MyType<version>, bạn có thể viết một hàm trong đó bạn có thể nắm bắt số phiên bản:

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

Vì vậy, bạn có thể làm những việc khác nhau tùy thuộc vào phiên bản của loại được truyền vào thay vì quá tải cho từng loại. Bạn cũng có thể có các hàm chuyển đổi nhận MyType<Version>và trả lại MyType<Version+1>, theo cách chung và thậm chí tái sử dụng chúng để có một ToNewest()hàm trả về phiên bản mới nhất của một loại từ bất kỳ phiên bản cũ nào (rất hữu ích cho các bản ghi có thể được lưu trữ trong một thời gian trước nhưng cần phải được xử lý với công cụ mới nhất ngày nay).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.