เมื่อมาถึงจุดนี้ห้องสมุดนี้เป็นการทดลองและเป็นความอยากรู้อยากเห็นที่บริสุทธิ์ ไม่มีการรับประกันความเสถียรของอินเทอร์เฟซหรือคุณภาพของการใช้งาน ใช้ตามความเสี่ยงของคุณเอง
Dyno แก้ปัญหาความหลากหลายของรันไทม์ได้ดีกว่าวานิลลา C ++ มันมีวิธีการกำหนดอินเทอร์เฟซที่สามารถเติมเต็มไม่ได้อย่างไม่เกี่ยวกับการใช้งานและให้วิธีการที่ปรับแต่งได้อย่างเต็มที่ในการจัดเก็บวัตถุ polymorphic และส่งไปยังวิธีการเสมือนจริง ไม่จำเป็นต้องมีการสืบทอดการจัดสรรกองหรือออกจากโลกที่สะดวกสบายของความหมายที่มีค่าและสามารถทำได้ในขณะที่มีประสิทธิภาพสูงกว่าวานิลลา C ++
Dyno คือการใช้ห้องสมุดที่บริสุทธิ์ของสิ่งที่เรียกว่าวัตถุลักษณะสนิมอินเทอร์เฟซ GO, คลาสประเภท Haskell และแนวคิดเสมือนจริง ภายใต้ประทุนนั้นใช้เทคนิค C ++ ที่รู้จักกันในชื่อประเภทการลบซึ่งเป็นแนวคิดเบื้องหลัง std::any , std::function และประเภทที่มีประโยชน์อื่น ๆ อีกมากมาย
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}อีกทางเลือกหนึ่งหากคุณพบว่าสิ่งนี้เป็นแผ่นหม้อไอน้ำมากเกินไปและคุณสามารถยืนโดยใช้แมโครได้สิ่งต่อไปนี้จะเทียบเท่า:
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}นี่คือห้องสมุด C ++ 17 จะไม่มีความพยายามในการสนับสนุนคอมไพเลอร์รุ่นเก่า (ขออภัย) ห้องสมุดเป็นที่รู้จักกันว่าทำงานร่วมกับคอมไพเลอร์ต่อไปนี้:
| ผู้ประกอบการ | รุ่น |
|---|---|
| GCC | > = 7 |
| เสียงดัง | > = 4.0 |
| แอปเปิ้ล | > = 9.1 |
ห้องสมุดขึ้นอยู่กับ boost.hana และ boost.callableTraits การทดสอบหน่วยขึ้นอยู่กับ libawful และเกณฑ์มาตรฐานขึ้นอยู่กับเกณฑ์มาตรฐาน Google, boost.typeerasure และ mpark.variant แต่คุณไม่ต้องการให้พวกเขาใช้ห้องสมุด สำหรับการพัฒนาในท้องถิ่นสคริปต์ dependencies/install.sh สามารถใช้ในการติดตั้งการอ้างอิงทั้งหมดโดยอัตโนมัติ
Dyno เป็นห้องสมุดเฉพาะส่วนหัวดังนั้นจึงไม่มีอะไรที่จะสร้างต่อไป เพียงเพิ่ม include/ ไดเรกทอรีลงในเส้นทางการค้นหาส่วนหัวของคอมไพเลอร์ของคุณ (และตรวจสอบให้แน่ใจว่าการพึ่งพานั้นเป็นที่พอใจ) และคุณก็พร้อมที่จะไป อย่างไรก็ตามมีการทดสอบหน่วยตัวอย่างและมาตรฐานที่สามารถสร้างได้:
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks ในการเขียนโปรแกรมความจำเป็นในการจัดการวัตถุที่มีอินเทอร์เฟซทั่วไป แต่มีประเภทไดนามิกที่แตกต่างกันเกิดขึ้นบ่อยมาก C ++ แก้ปัญหานี้ด้วยการสืบทอด:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}อย่างไรก็ตามวิธีการนี้มีข้อเสียหลายประการ มันคือ
ที่ล่วงล้ำ
เพื่อให้ Square และ Circle เพื่อเติมเต็มอินเทอร์เฟซ Drawable พวกเขาทั้งคู่จำเป็นต้องได้รับมรดกจากคลาสฐาน Drawable สิ่งนี้ต้องมีใบอนุญาตในการปรับเปลี่ยนคลาสเหล่านั้นซึ่งทำให้การสืบทอดเป็นไปได้มาก ตัวอย่างเช่นคุณจะสร้าง std::vector<int> เติมเต็มอินเทอร์เฟซ Drawable ได้อย่างไร คุณทำไม่ได้
เข้ากันไม่ได้กับความหมายของมูลค่า
มรดกต้องการให้คุณผ่านพอยน์เตอร์ polymorphic หรือการอ้างอิงไปยังวัตถุแทนที่จะเป็นวัตถุเองซึ่งเล่นได้แย่มากกับส่วนที่เหลือของภาษาและไลบรารีมาตรฐาน ตัวอย่างเช่นคุณจะคัดลอกเวกเตอร์ของ Drawable S ได้อย่างไร? คุณต้องจัดเตรียมวิธี clone() แต่ตอนนี้คุณเพิ่งทำอินเทอร์เฟซของคุณ
ควบคู่ไปกับการจัดเก็บแบบไดนามิกอย่างแน่นหนา
เนื่องจากขาดความหมายที่มีค่าเรามักจะจัดสรรวัตถุ polymorphic เหล่านี้บนกอง นี่เป็นทั้งประสิทธิภาพที่ไม่มีประสิทธิภาพและผิดพลาดเนื่องจากโอกาสที่เราไม่จำเป็นต้องใช้ระยะเวลาการจัดเก็บแบบไดนามิกเลยและวัตถุที่มีระยะเวลาการจัดเก็บอัตโนมัติ (เช่นสแต็ก) จะเพียงพอแล้ว
ป้องกันการ inlining
95% ของเวลาเราจบลงด้วยการเรียกวิธีการเสมือนจริงผ่านตัวชี้ polymorphic หรือการอ้างอิง ที่ต้องใช้สามการยืนยัน: หนึ่งสำหรับการโหลดตัวชี้ไปยัง vtable ภายในวัตถุหนึ่งสำหรับโหลดรายการที่เหมาะสมใน vtable และอีกหนึ่งสำหรับการเรียกทางอ้อมไปยังตัวชี้ฟังก์ชั่น การกระโดดทั้งหมดนี้ทำให้คอมไพเลอร์เป็นเรื่องยากที่จะทำการตัดสินใจที่ดี อย่างไรก็ตามปรากฎว่าทางอ้อมทั้งหมดเหล่านี้ยกเว้นการโทรทางอ้อมสามารถหลีกเลี่ยงได้
น่าเสียดายที่นี่เป็นตัวเลือกที่ C ++ ทำเพื่อเราและนี่คือกฎที่เราต้องผูกพันเมื่อเราต้องการความหลากหลายแบบไดนามิก หรือว่าจริงเหรอ?
Dyno แก้ปัญหาความหลากหลายของรันไทม์ใน C ++ โดยไม่มีข้อเสียใด ๆ ที่ระบุไว้ข้างต้นและสารพัดอื่น ๆ อีกมากมาย มันคือ:
ไม่ล่วงล้ำ
อินเทอร์เฟซสามารถเติมเต็มได้โดยประเภทโดยไม่ต้องมีการดัดแปลงใด ๆ กับประเภทนั้น Heck ประเภทสามารถเติมเต็มอินเทอร์เฟซเดียวกันได้ในรูปแบบที่แตกต่างกัน! ด้วย Dyno คุณสามารถจูบลำดับชั้นของชั้นเรียนที่ไร้สาระ
100% ขึ้นอยู่กับความหมายของมูลค่า
วัตถุโพลีมอร์ฟิคสามารถส่งผ่านตามความหมายตามธรรมชาติของพวกเขา คุณต้องคัดลอกวัตถุ polymorphic ของคุณหรือไม่? แน่นอนเพียงตรวจสอบให้แน่ใจว่าพวกเขามีตัวสร้างสำเนา คุณต้องการให้แน่ใจว่าพวกเขาไม่ได้รับการคัดลอก? แน่นอนว่าทำเครื่องหมายว่ามันถูกลบ ด้วย dyno วิธี clone() และการเพิ่มจำนวนของพอยน์เตอร์ใน API เป็นสิ่งที่ผ่านมา
ไม่ได้ประกอบกับกลยุทธ์การจัดเก็บเฉพาะใด ๆ
วิธีการจัดเก็บวัตถุ polymorphic นั้นเป็นรายละเอียดการใช้งานจริง ๆ และไม่ควรรบกวนวิธีการใช้วัตถุนั้น Dyno ช่วยให้คุณควบคุมได้อย่างสมบูรณ์เกี่ยวกับวิธีการจัดเก็บวัตถุของคุณ คุณมีวัตถุ polymorphic ขนาดเล็กมากมาย? แน่นอนให้เก็บไว้ในบัฟเฟอร์ท้องถิ่นและหลีกเลี่ยงการจัดสรรใด ๆ หรือบางทีมันอาจจะสมเหตุสมผลสำหรับคุณที่จะเก็บของบนกอง? แน่นอนไปข้างหน้า
กลไกการจัดส่งที่ยืดหยุ่นเพื่อให้ได้ประสิทธิภาพที่ดีที่สุดเท่าที่จะเป็นไปได้
การจัดเก็บตัวชี้ไปยัง vtable เป็นเพียงหนึ่งในกลยุทธ์การใช้งานที่แตกต่างกันมากมายสำหรับการดำเนินการส่งแบบไดนามิก Dyno ให้คุณควบคุมได้อย่างสมบูรณ์ว่าการส่งแบบไดนามิกเกิดขึ้นได้อย่างสมบูรณ์และในความเป็นจริงสามารถเอาชนะ VTables ได้ในบางกรณี หากคุณมีฟังก์ชั่นที่เรียกว่าในลูปร้อนคุณสามารถจัดเก็บได้โดยตรงในวัตถุและข้ามทางอ้อม vtable นอกจากนี้คุณยังสามารถใช้ความรู้เฉพาะแอปพลิเคชันที่คอมไพเลอร์ไม่จำเป็นต้องเพิ่มประสิทธิภาพการโทรแบบไดนามิกบางอย่าง-devirtualization ระดับห้องสมุด
ก่อนอื่นคุณเริ่มต้นด้วยการกำหนดอินเทอร์เฟซทั่วไปและตั้งชื่อ Dyno ให้ภาษาเฉพาะโดเมนง่ายๆที่จะทำเช่นนั้น ตัวอย่างเช่นลองกำหนดอินเทอร์เฟ Drawable ที่สามารถอธิบายประเภทที่สามารถวาดได้:
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { }; สิ่งนี้กำหนดได้ Drawable ว่าเป็นตัวแทนของอินเทอร์เฟซสำหรับทุกสิ่งที่มีวิธีการที่เรียกว่า draw นำการอ้างอิงไปยัง std::ostream Dyno เรียก แนวคิดแบบไดนามิก อินเทอร์เฟซเหล่านี้เนื่องจากพวกเขาอธิบายชุดของข้อกำหนดที่จะปฏิบัติตามประเภท (เช่นแนวคิด C ++) อย่างไรก็ตามแตกต่างจากแนวคิด C ++ แนวคิดแบบไดนามิก เหล่านี้ใช้ในการสร้างอินเตอร์เฟสรันไทม์ดังนั้นชื่อ ไดนามิก คำจำกัดความข้างต้นนั้นเทียบเท่ากับสิ่งต่อไปนี้:
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};เมื่อมีการกำหนดอินเทอร์เฟซแล้วขั้นตอนต่อไปคือการสร้างประเภทที่ตรงกับอินเทอร์เฟซนี้ ด้วยการสืบทอดคุณจะเขียนอะไรแบบนี้:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};ด้วย dyno polymorphism ไม่ได้เป็นการล่วงล้ำและมีให้บริการผ่านสิ่งที่เรียกว่า แผนที่แนวคิด (หลังจากแผนที่แนวคิด C ++ 0x):
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);โครงสร้างนี้เป็นความเชี่ยวชาญของเทมเพลตตัวแปร C ++ 14 ชื่อ
concept_mapที่กำหนดไว้ในdyno::Namespace จากนั้นเราเริ่มต้นความเชี่ยวชาญนั้นด้วยdyno::make_concept_map(...)
พารามิเตอร์แรกของแลมบ์ดาคือความหมาย *this ที่บอกเป็นนัยเมื่อเราประกาศ draw เป็นวิธีการด้านบน นอกจากนี้ยังเป็นไปได้ที่จะลบฟังก์ชั่นที่ไม่ใช่สมาชิก (ดูส่วนที่เกี่ยวข้อง)
แผนที่แนวคิด นี้กำหนดวิธีการที่ Square ประเภทตรงกับแนวคิด Drawable ในแง่หนึ่งมัน แมปสแคว Square ประเภทกับการใช้แนวคิดซึ่งเป็นแรงจูงใจในการจัดตั้ง เมื่อประเภทที่ตรงตามข้อกำหนดของแนวคิดเราบอกว่า แบบจำลอง ประเภท (หรือเป็นแบบจำลองของ) แนวคิดนั้น ตอนนี้ Square เป็นแบบจำลองของแนวคิด Drawable เราต้องการใช้รูป Square เป็น Drawable ด้วยการสืบทอดแบบดั้งเดิมเราจะใช้ตัวชี้ไปยังคลาสฐานเช่นนี้:
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{}); ด้วย dyno ความหลากหลายและความหมายของค่านั้นเข้ากันได้และวิธีการส่งผ่านประเภท polymorphic สามารถปรับแต่งได้สูง ในการทำเช่นนี้เราจะต้องกำหนดประเภทที่สามารถเก็บสิ่งใดก็ได้ที่ Drawable มันเป็นประเภทนั้นแทนที่จะเป็นสิ่ง Drawable* ว่าเราจะผ่านไปรอบ ๆ และกลับจากฟังก์ชั่น polymorphic เพื่อช่วยกำหนด wrapper นี้ Dyno จัดเตรียม dyno::poly Container ซึ่งสามารถเก็บวัตถุโดยพลการที่สอดคล้องกับแนวคิดที่กำหนด อย่างที่คุณจะเห็น dyno::poly มีบทบาทสองอย่าง: มันเก็บวัตถุ polymorphic และดูแลการส่งแบบไดนามิกของวิธีการ สิ่งที่คุณต้องทำคือเขียนเสื้อคลุมบาง ๆ เหนือ dyno::poly เพื่อให้อินเทอร์เฟซที่ต้องการ:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};หมายเหตุ: คุณสามารถใช้
dyno::polyในอินเทอร์เฟซของคุณได้โดยตรง อย่างไรก็ตามมันสะดวกกว่ามากในการใช้ wrapper ด้วยวิธีการจริงมากกว่าdyno::polyและแนะนำให้เขียน wrapper
มาทำลายสิ่งนี้กันเถอะ ก่อนอื่นเรากำหนดสมาชิก poly_ นั่นคือคอนเทนเนอร์ polymorphic สำหรับทุกสิ่งที่เป็นแบบจำลองแนวคิด Drawable :
dyno::poly<Drawable> poly_; จากนั้นเรากำหนดตัวสร้างที่อนุญาตให้สร้างคอนเทนเนอร์นี้จากประเภท T :
template < typename T>
drawable (T x) : poly_{x} { } สมมติฐานที่ไม่ได้กล่าวถึงที่นี่คือ T จำลองแนวคิด Drawable แน่นอนเมื่อคุณสร้าง dyno::poly จากวัตถุประเภท T , Dyno จะไปดูแผนที่แนวคิดที่กำหนดไว้สำหรับ Drawable และ T ถ้ามี หากไม่มีแผนที่แนวคิดดังกล่าวห้องสมุดจะรายงานว่าเรากำลังพยายามสร้าง dyno::poly จากประเภทที่ไม่รองรับและโปรแกรมของคุณจะไม่รวบรวม
ในที่สุดส่วนที่แปลกประหลาดและสำคัญที่สุดของคำจำกัดความข้างต้นคือวิธี draw :
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); } สิ่งที่เกิดขึ้นที่นี่คือเมื่อ .draw ถูกเรียกใช้วัตถุที่เรา drawable เราจะทำการส่งแบบไดนามิกเพื่อการใช้งานฟังก์ชั่น "draw" สำหรับวัตถุที่เก็บไว้ในปัจจุบันใน dyno::poly และเรียกว่า ตอนนี้เพื่อสร้างฟังก์ชั่นที่ยอมรับสิ่งใดก็ตามที่ Drawable ไม่จำเป็นต้องกังวลเกี่ยวกับพอยน์เตอร์และความเป็นเจ้าของในอินเทอร์เฟซของคุณอีกต่อไป:
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});ถ้าคุณคิดว่านี่เป็นเรื่องโง่และคุณควรใช้เทมเพลตคุณก็พูดถูก อย่างไรก็ตามให้พิจารณาสิ่งต่อไปนี้ที่ซึ่งคุณต้องการความหลากหลายของ รันไทม์ :
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable()); การพูดอย่างเคร่งครัดคุณไม่จำเป็นต้องห่อ dyno::poly แต่การทำเช่นนั้นทำให้เกิดอุปสรรคที่ดีระหว่าง Dyno และส่วนที่เหลือของรหัสของคุณซึ่งไม่ต้องกังวลว่าจะใช้เลเยอร์ polymorphic ของคุณอย่างไร นอกจากนี้เรายังเพิกเฉยต่อวิธีการที่ dyno::poly ถูกนำไปใช้ในคำจำกัดความข้างต้น อย่างไรก็ตาม dyno::poly เป็นคอนเทนเนอร์ตามนโยบายที่ทรงพลังมากสำหรับวัตถุ polymorphic ที่สามารถปรับแต่งได้ตามความต้องการด้านประสิทธิภาพ การสร้าง wrapper drawable ทำให้ง่ายต่อการปรับแต่งกลยุทธ์การใช้งานที่ใช้โดย dyno::poly เพื่อประสิทธิภาพโดยไม่ส่งผลกระทบต่อรหัสที่เหลือของคุณ
แง่มุมแรกที่สามารถปรับแต่งใน dyno::poly เป็นวิธีที่วัตถุถูกเก็บไว้ภายในคอนเทนเนอร์ โดยค่าเริ่มต้นเราเพียงจัดเก็บตัวชี้ไปยังวัตถุจริงเช่นเดียวกับที่จะทำกับ polymorphism ตามมรดก อย่างไรก็ตามนี่ไม่ใช่การใช้งานที่มีประสิทธิภาพมากที่สุดและนั่นคือเหตุผลที่ dyno::poly อนุญาตให้ปรับแต่งได้ ในการทำเช่นนั้นเพียงส่งนโยบายการจัดเก็บไปยัง dyno::poly ตัวอย่างเช่นเรามากำหนด wrapper drawable ของเราเพื่อให้พยายามจัดเก็บวัตถุได้ถึง 16 ไบต์ในบัฟเฟอร์ท้องถิ่น แต่จากนั้นจะกลับไปที่กองถ้าวัตถุมีขนาดใหญ่กว่า:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
}; ขอให้สังเกตว่าไม่มีอะไรยกเว้นนโยบายที่เปลี่ยนไปในคำจำกัดความของเรา นั่นคือทฤษฎีที่สำคัญมากของ dyno ; นโยบายเหล่านี้เป็นรายละเอียดการใช้งานและไม่ควรเปลี่ยนวิธีการเขียนรหัสของคุณ ด้วยคำนิยามข้างต้นตอนนี้คุณสามารถสร้าง S drawable เหมือนที่คุณเคยทำมาก่อนและจะไม่มีการจัดสรรที่จะเกิดขึ้นเมื่อวัตถุที่คุณสร้างได้จาก drawable ที่พอดีใน 16 ไบต์ อย่างไรก็ตามเมื่อมันไม่พอดี dyno::poly จะจัดสรรบัฟเฟอร์ขนาดใหญ่พอบนกอง
สมมติว่าคุณไม่ต้องการจัดสรร ไม่มีปัญหาเพียงแค่เปลี่ยนนโยบายเป็น dyno::local_storage<16> หากคุณพยายามสร้างวัตถุ drawable จากวัตถุที่มีขนาดใหญ่เกินไปที่จะพอดีกับที่เก็บข้อมูลในท้องถิ่นโปรแกรมของคุณจะไม่รวบรวม ไม่เพียง แต่เราจะบันทึกการจัดสรรเท่านั้น แต่เรายังช่วยประหยัดตัวชี้ทางอ้อมทุกครั้งที่เราเข้าถึงวัตถุ polymorphic หากเราเปรียบเทียบกับวิธีการสืบทอดแบบดั้งเดิม ด้วยการปรับแต่งรายละเอียดการใช้งาน (สำคัญ) เหล่านี้สำหรับกรณีการใช้งานเฉพาะของคุณคุณสามารถทำให้โปรแกรมของคุณมีประสิทธิภาพมากกว่าด้วยการสืบทอดแบบคลาสสิก
มีนโยบายการจัดเก็บอื่น ๆ เช่น dyno::remote_storage และ dyno::non_owning_storage dyno::remote_storage เป็นค่าเริ่มต้นซึ่งมักจะเก็บตัวชี้ไปยังวัตถุที่จัดสรรฮีปเสมอ dyno::non_owning_storage เก็บตัวชี้ไปยังวัตถุที่มีอยู่แล้วโดยไม่ต้องกังวลเกี่ยวกับอายุการใช้งานของวัตถุนั้น ช่วยให้การใช้มุมมอง polymorphic ที่ไม่เป็นเจ้าของเหนือวัตถุซึ่งมีประโยชน์มาก
นโยบายการจัดเก็บที่กำหนดเองสามารถสร้างได้ค่อนข้างง่าย ดู <dyno/storage.hpp> สำหรับรายละเอียด
เมื่อเราแนะนำ dyno::poly เราบอกว่ามันมีสองบทบาท; สิ่งแรกคือการจัดเก็บวัตถุ polymorphic และอันที่สองคือการจัดส่งแบบไดนามิก เช่นเดียวกับที่เก็บข้อมูลสามารถปรับแต่งได้วิธีการจัดส่งแบบไดนามิกสามารถปรับแต่งได้โดยใช้นโยบาย ตัวอย่างเช่นเรามากำหนด wrapper drawable ของเราเพื่อให้แทนที่จะเก็บตัวชี้ไปยัง vtable มันแทนที่จะเก็บ vtable ไว้ในวัตถุ drawable เอง ด้วยวิธีนี้เราจะหลีกเลี่ยงทางอ้อมหนึ่งครั้งในแต่ละครั้งที่เราเข้าถึงฟังก์ชั่นเสมือนจริง:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
}; ขอให้สังเกตว่าไม่มีอะไรนอกเหนือจากนโยบาย VTable ที่จำเป็นต้องเปลี่ยนในคำจำกัดความของประเภทที่เรา drawable นอกจากนี้หากเราต้องการเราสามารถเปลี่ยนนโยบายการจัดเก็บข้อมูลได้อย่างอิสระจากนโยบาย vtable ด้วยข้างต้นแม้ว่าเราจะประหยัดการยืนยันทั้งหมด แต่เราก็จ่ายเงินด้วยการทำให้วัตถุ drawable ของเรามีขนาดใหญ่ขึ้น (เนื่องจากจำเป็นต้องถือ vtable ในเครื่อง) นี่อาจเป็นสิ่งต้องห้ามหากเรามีฟังก์ชั่นมากมายใน VTable แต่จะทำให้รู้สึกมากขึ้นในการจัดเก็บ vtable ส่วนใหญ่จากระยะไกล แต่เฉพาะฟังก์ชั่นบางอย่างที่เราเรียกอย่างหนัก Dyno ทำให้ง่ายต่อการทำเช่นนั้นโดยใช้ ตัวเลือก ซึ่งสามารถใช้ในการปรับแต่งฟังก์ชั่นที่นโยบายใช้กับ:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
}; ให้คำจำกัดความนี้ VTable จะถูกแบ่งออกเป็นสอง ส่วนแรกคือท้องถิ่นกับวัตถุ drawable และมีเฉพาะวิธี draw ส่วนที่สองเป็นตัวชี้ไปยัง vtable ในที่เก็บข้อมูลคงที่ซึ่งถือวิธีที่เหลือ (ตัวอย่างเช่น destructor)
Dyno จัดทำนโยบาย vtable สองแบบ dyno::local<> และ dyno::remote<> นโยบายทั้งสองนี้จะต้องปรับแต่งโดยใช้ ตัวเลือก ตัวเลือกที่ได้รับการสนับสนุนโดยห้องสมุดคือ dyno::only<functions...> , dyno::except<...> , และ dyno::everything_else (ซึ่งสามารถสะกด dyno::everything )
เมื่อกำหนดแนวคิดมักจะเป็นกรณีที่เราสามารถให้คำจำกัดความเริ่มต้นสำหรับฟังก์ชั่นบางอย่างที่เกี่ยวข้องกับแนวคิด ตัวอย่างเช่นโดยค่าเริ่มต้นมันอาจจะสมเหตุสมผลที่จะใช้ฟังก์ชั่นสมาชิกชื่อ draw (ถ้ามี) เพื่อใช้วิธีการ "draw" แบบนามธรรมของแนวคิด Drawable สำหรับสิ่งนี้เราสามารถใช้ dyno::default_concept_map :
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
); ตอนนี้เมื่อใดก็ตามที่เราพยายามที่จะดูว่าบางประเภท T เป็นไปตามแนวคิด Drawable เราจะกลับไปที่แผนที่แนวคิดเริ่มต้นหากไม่มีการกำหนดแผนที่แนวคิด ตัวอย่างเช่นเราสามารถสร้าง Circle ประเภทใหม่:
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle" Circle เป็นแบบจำลอง Drawable โดยอัตโนมัติแม้ว่าเราจะไม่ได้กำหนดแผนที่แนวคิดสำหรับ Circle อย่างชัดเจน ในทางกลับกันถ้าเราจะกำหนดแผนที่แนวคิดดังกล่าวมันจะมีความสำคัญกว่าค่าเริ่มต้น:
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle" บางครั้งมันก็มีประโยชน์ในการกำหนดแผนที่แนวคิดสำหรับตระกูลที่สมบูรณ์แบบทั้งหมดในครั้งเดียว ตัวอย่างเช่นเราอาจต้องการสร้าง std::vector<T> แบบจำลอง Drawable แต่เฉพาะเมื่อ T สามารถพิมพ์ไปยังสตรีมได้ สิ่งนี้ทำได้ง่ายโดยใช้เคล็ดลับความลับ (ไม่เป็นเช่นนั้น):
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "สังเกตว่าเราไม่จำเป็นต้องแก้ไข
std::vectorเลย เราจะทำสิ่งนี้กับ polymorphism คลาสสิกได้อย่างไร? คำตอบ: ไม่สามารถทำได้
Dyno อนุญาตให้ลบฟังก์ชั่นและฟังก์ชั่นที่ไม่ใช่สมาชิกที่ถูกส่งไปยังอาร์กิวเมนต์โดยพลการ (แต่มีเพียงอาร์กิวเมนต์เดียว) ด้วย ในการทำเช่นนี้เพียงกำหนดแนวคิดโดยใช้ dyno::function แทน dyno::method และใช้ตัวยึดตำแหน่ง dyno::T เพื่อแสดงถึงการโต้แย้งที่ถูกลบ:
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { }; dyno::T const& พารามิเตอร์ที่ใช้ด้านบนแสดงถึงประเภทของวัตถุที่เรียกใช้ฟังก์ชัน อย่างไรก็ตามไม่จำเป็นต้องเป็นพารามิเตอร์แรก:
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };การปฏิบัติตามแนวคิดไม่เปลี่ยนแปลงไม่ว่าจะเป็นแนวคิดที่ใช้วิธีการหรือฟังก์ชั่น แต่ตรวจสอบให้แน่ใจว่าพารามิเตอร์ของการใช้งานฟังก์ชั่นของคุณนั้นตรงกับฟังก์ชั่นที่ประกาศไว้ในแนวคิด:
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
); ในที่สุดเมื่อเรียก function บน dyno::poly คุณจะต้องส่งผ่านพารามิเตอร์ทั้งหมดอย่างชัดเจนเนื่องจาก Dyno ไม่สามารถเดาได้ว่าคุณต้องการส่งแบบใด พารามิเตอร์ที่ประกาศด้วยตัวยึดตำแหน่ง dyno::T ในแนวคิดควรส่งผ่าน dyno::poly เอง:
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};