ฉันเคยเห็นตัวอย่างของ C ++ โดยใช้พารามิเตอร์แม่แบบแม่แบบ (นั่นคือแม่แบบที่ใช้แม่แบบเป็นพารามิเตอร์) เพื่อทำการออกแบบคลาสตามนโยบาย เทคนิคนี้มีประโยชน์อื่น ๆ อีกบ้าง?
ฉันเคยเห็นตัวอย่างของ C ++ โดยใช้พารามิเตอร์แม่แบบแม่แบบ (นั่นคือแม่แบบที่ใช้แม่แบบเป็นพารามิเตอร์) เพื่อทำการออกแบบคลาสตามนโยบาย เทคนิคนี้มีประโยชน์อื่น ๆ อีกบ้าง?
คำตอบ:
ฉันคิดว่าคุณต้องใช้ไวยากรณ์แม่แบบแม่แบบเพื่อส่งผ่านพารามิเตอร์ที่มีประเภทเป็นแม่แบบขึ้นอยู่กับแม่แบบอื่นเช่นนี้:
template <template<class> class H, class S>
void f(const H<S> &value) {
}
นี่H
คือแม่แบบ H
แต่ฉันต้องการฟังก์ชั่นนี้เพื่อจัดการกับความเชี่ยวชาญทั้งหมดของ
หมายเหตุ : ฉันเขียนโปรแกรม 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;
}
หมายเหตุ : std::vector
มีพารามิเตอร์เทมเพลตสองแบบประเภทและตัวจัดสรรดังนั้นเราจึงต้องยอมรับทั้งสองอย่าง โชคดีเนื่องจากการลดประเภทเราไม่จำเป็นต้องเขียนประเภทที่แน่นอนออกมาอย่างชัดเจน
ซึ่งคุณสามารถใช้เช่นนี้
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;
}
ซึ่งเป็นวิธีที่ฉันต้องการเขียนโค้ดประเภทนี้
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
f<vector<int>>
f<vector,int>
วิธีการf<ATemplate,AType>
, f<vector<int>>
วิธีการf<AType>
ที่จริงแล้ว usecase สำหรับพารามิเตอร์แม่แบบค่อนข้างชัดเจน เมื่อคุณเรียนรู้ว่า 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 ตัวสำหรับประเภทค่าและตัวจัดสรร ในขณะที่ตัวจัดสรรถูกตั้งค่าเริ่มต้นเราก็ยังควรคำนึงถึงการมีอยู่ของมันในผู้ให้บริการเทมเพลตของเรา <<:
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
Voila ที่จะทำงานโดยอัตโนมัติสำหรับคอนเทนเนอร์ลำดับปัจจุบันและอนาคตทั้งหมดที่ปฏิบัติตามโปรโตคอลมาตรฐาน ในการเพิ่มแผนที่ลงในมิกซ์นั้นจะต้องดูที่การอ้างอิงเพื่อรับทราบว่าพวกเขายอมรับเทมเพลต 4 รายการดังนั้นเราจึงต้องการรุ่นโอเปอเรเตอร์ << ด้านบนด้วยเทมเพลตแม่แบบ 4-ARG นอกจากนี้เรายังเห็นว่า std: pair พยายามแสดงผลด้วยตัวดำเนินการ 2-ARG << สำหรับประเภทลำดับที่เรากำหนดไว้ก่อนหน้านี้ดังนั้นเราจะให้ความเชี่ยวชาญเฉพาะสำหรับ std :: pair
Btw ด้วย C + 11 ซึ่งอนุญาตเทมเพลต Variadic (และควรอนุญาตให้ใช้เท็มเพลตเทมเพลต Variadic) จึงเป็นไปได้ที่จะมีโอเปอเรเตอร์เดียว << เพื่อควบคุมทั้งหมด ตัวอย่างเช่น:
#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
__PRETTY_FUNCTION__
ซึ่งเหนือสิ่งอื่นใดรายงานคำอธิบายพารามิเตอร์เทมเพลตในข้อความธรรมดา เสียงดังกราวทำมันเช่นกัน บางครั้งคุณสมบัติที่มีประโยชน์มากที่สุด (ตามที่คุณเห็น)
นี่คือตัวอย่างง่ายๆที่นำมาจาก'การออกแบบ Modern C ++ - การเขียนโปรแกรมและรูปแบบการออกแบบทั่วไปประยุกต์'โดย Andrei Alexandrescu:
เขาใช้คลาสที่มีพารามิเตอร์เท็มเพลตเทมเพลตเพื่อนำรูปแบบนโยบายไปใช้:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
เขาอธิบาย: โดยทั่วไปแล้วคลาสโฮสต์รู้อยู่แล้วหรือสามารถอนุมานอาร์กิวเมนต์เทมเพลตของคลาสนโยบายได้อย่างง่ายดาย ในตัวอย่างด้านบน WidgetManager จัดการวัตถุประเภท Widget เสมอดังนั้นผู้ใช้ต้องระบุ Widget อีกครั้งในการสร้างอินสแตนซ์ของ CreationPolicy ซ้ำซ้อนและอาจเป็นอันตรายในกรณีนี้รหัสไลบรารีสามารถใช้พารามิเตอร์แม่แบบแม่แบบสำหรับการระบุนโยบาย
ผลที่ได้คือรหัสลูกค้าสามารถใช้ 'WidgetManager' ได้อย่างสง่างาม:
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
แทนที่จะเป็นวิธีที่ยุ่งยากและผิดพลาดมากกว่าที่คำจำกัดความที่ไม่มีอาร์กิวเมนต์เท็มเพลตเทมเพลตจะต้องมี:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
นี่เป็นอีกหนึ่งตัวอย่างในทางปฏิบัติจากฉันห้องสมุดเครือข่ายประสาท CUDA Convolutional ฉันมีแม่แบบชั้นเรียนต่อไปนี้:
template <class T> class Tensor
ซึ่งจริง ๆ แล้วใช้การจัดการเมทริกซ์ n มิติ นอกจากนี้ยังมีเทมเพลตสำหรับเด็ก:
template <class T> class TensorGPU : public Tensor<T>
ซึ่งใช้ฟังก์ชันการทำงานเดียวกัน แต่ใช้ GPU เทมเพลตทั้งสองสามารถทำงานกับประเภทพื้นฐานทั้งหมดเช่นโฟลต, 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>
ซึ่งจะมีทั้งน้ำหนักและอินพุตของประเภทลอยและบน GPU แต่ connection_matrix จะเป็น int เสมอไม่ว่าจะเป็นบน CPU (โดยระบุ TT = Tensor) หรือบน GPU (โดยระบุ TT = TensorGPU)
สมมติว่าคุณกำลังใช้ 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;
โปรดทราบว่าคุณกำลังกำจัดโดยตรงให้พารามิเตอร์แม่แบบอื่น ๆ เพื่อแม่แบบที่ได้รับ ; "อินเตอร์เฟซ" ยังคงได้รับพวกเขา
สิ่งนี้ยังช่วยให้คุณสร้าง typedefs ใน "ส่วนต่อประสาน" ที่ขึ้นอยู่กับพารามิเตอร์ประเภทซึ่งจะสามารถเข้าถึงได้จากเทมเพลตที่ได้รับ
typedef ด้านบนใช้งานไม่ได้เนื่องจากคุณไม่สามารถพิมพ์ไปยังเทมเพลตที่ไม่ระบุ วิธีนี้ใช้ได้ผล ((และ C ++ 11 มีการสนับสนุนดั้งเดิมสำหรับเทมเพลต typedefs):
template <typename VALUE>
struct derived_interface_type {
typedef typename interface<derived, VALUE> type;
};
typedef typename derived_interface_type<int>::type derived_t;
คุณต้องมีหนึ่ง__image_typeสำหรับแต่ละการสร้างอินสแตนซ์ของเทมเพลตที่ได้รับมาอย่างน่าเสียดายเว้นแต่มีกลอุบายอื่น ๆ
derived
จะใช้คลาสเทมเพลตได้อย่างไรโดยไม่มีอาร์กิวเมนต์ของเทมเพลตเช่นบรรทัดtypedef typename interface<derived, VALUE> type;
template <typename>
มันทำงานโดยทั่วไปเพราะแม่แบบที่สอดคล้องกันพารามิเตอร์กำลังเต็มไปถูกกำหนดให้เป็น ในแง่หนึ่งคุณสามารถนึกถึงพารามิเตอร์เทมเพลตว่ามี 'metatype' เมตาไทป์ปกติสำหรับพารามิเตอร์เทมเพลตคือtypename
ซึ่งหมายความว่าต้องกรอกด้วยประเภทปกติ template
วิธี metatype จะต้องเต็มไปด้วยการอ้างอิงถึงแม่แบบ derived
กำหนดเทมเพลตที่ยอมรับtypename
พารามิเตอร์ metatyped หนึ่งพารามิเตอร์ดังนั้นจึงเหมาะกับการเรียกเก็บเงินและสามารถอ้างอิงได้ที่นี่ ทำให้รู้สึก?
typedef
อยู่ นอกจากนี้คุณสามารถหลีกเลี่ยงสิ่งที่ซ้ำกันint
ในตัวอย่างแรกของคุณโดยใช้โครงสร้างมาตรฐานเช่น a value_type
ในประเภท DERIVED
typedef
แก้ไขปัญหาได้จากบล็อก 2 แต่จุดที่ 2 นั้นถูกต้องฉันคิดว่า ... ใช่นั่นอาจเป็นวิธีที่ง่ายกว่าในการทำสิ่งเดียวกัน
นี่คือสิ่งที่ฉันวิ่งเข้าไป:
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();
}
ในการแก้ปัญหาด้วยเทมเพลต variadic ที่จัดทำโดย pfalcon ฉันพบว่ามันยากที่จะเชี่ยวชาญตัวดำเนินการ ostream สำหรับ std :: map เนื่องจากลักษณะโลภของความเชี่ยวชาญของ variadic นี่คือการแก้ไขเล็กน้อยที่ใช้งานได้สำหรับฉัน:
#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;
}
นี่คือข้อสรุปจากสิ่งที่ฉันเพิ่งใช้ ฉันโพสต์มันเพราะมันเป็นตัวอย่างที่ง่ายมากและมันแสดงให้เห็นถึงกรณีการใช้งานจริงพร้อมกับอาร์กิวเมนต์เริ่มต้น:
#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)
สมมติว่าคุณผ่านในจำนวนเต็มพูด สำหรับกรณีก่อนหน้าเทมเพลตจะถูกสร้างอินสแตนซ์โดยคอมไพเลอร์ซึ่งจะบ่นเกี่ยวกับการใช้งานของc
ใน for for loop ส่วนหลังจะไม่สร้างอินสแตนซ์ของเทมเพลตเลยเนื่องจากไม่พบประเภทการจับคู่
โดยทั่วไปถ้าคลาส / ฟังก์ชันเทมเพลตของคุณถูกออกแบบมาเพื่อจัดการคลาสเทมเพลตเป็นพารามิเตอร์เทมเพลตมันจะดีกว่าถ้าจะให้ชัดเจน
ฉันใช้สำหรับรุ่นที่กำหนดเวอร์ชันแล้ว
หากคุณมีประเภทเวอร์ชันผ่านเทมเพลตเช่น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()
ฟังก์ชั่นที่ส่งคืนเวอร์ชันล่าสุดของประเภทจากเวอร์ชันเก่า ๆ (มีประโยชน์มากสำหรับบันทึกที่อาจถูกจัดเก็บในขณะที่ย้อนหลัง แต่ต้องถูกประมวลผลด้วยเครื่องมือใหม่ล่าสุดของวันนี้)