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?
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?
Câu trả lời:
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, H
là 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::vector
có 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.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
và không f<vector<int>>
.
f<vector,int>
phương tiện f<ATemplate,AType>
, f<vector<int>>
phương tiệnf<AType>
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
__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).
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;
Đâ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).
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.
derived
có 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;
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 template
phươ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 typename
tham 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ý?
typedef
. Ngoài ra, bạn có thể tránh trùng lặp int
trong 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_type
trong kiểu DERIVED.
typedef
quyế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ự.
Đâ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();
}
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;
}
Đâ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;
};
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 c
trong 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.
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).