CRTP เพื่อหลีกเลี่ยงความหลากหลายแบบไดนามิก


คำตอบ:


142

มีสองวิธี

สิ่งแรกคือการระบุอินเทอร์เฟซแบบคงที่สำหรับโครงสร้างประเภท:

template <class Derived>
struct base {
  void foo() {
    static_cast<Derived *>(this)->foo();
  };
};

struct my_type : base<my_type> {
  void foo(); // required to compile.
};

struct your_type : base<your_type> {
  void foo(); // required to compile.
};

ประการที่สองคือการหลีกเลี่ยงการใช้สำนวนอ้างอิงถึงฐานหรือตัวชี้ไปที่ฐานและทำการเดินสายตามเวลาคอมไพล์ เมื่อใช้คำจำกัดความข้างต้นคุณสามารถมีฟังก์ชันเทมเพลตที่มีลักษณะดังนี้:

template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
  obj.foo(); // will do static dispatch
}

struct not_derived_from_base { }; // notice, not derived from base

// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload

ดังนั้นการรวมนิยามโครงสร้าง / อินเทอร์เฟซและการหักประเภทเวลาคอมไพล์ในฟังก์ชันของคุณช่วยให้คุณทำการจัดส่งแบบคงที่แทนการจัดส่งแบบไดนามิก นี่คือสาระสำคัญของความหลากหลายแบบคงที่


5
ขอย้ำnot_derived_from_baseว่าไม่ได้มาจากbaseและไม่ได้มาจากbase...
leftaround ประมาณ

3
อันที่จริงไม่จำเป็นต้องมีการประกาศ foo () ใน my_type / your_type codepad.org/ylpEm1up (ทำให้เกิด stack overflow) - มีวิธีบังคับใช้คำจำกัดความของ foo ในเวลาคอมไพล์หรือไม่? - ตกลงพบวิธีแก้ไขแล้ว: ideone.com/C6Oz9 - บางทีคุณอาจต้องการแก้ไขในคำตอบของคุณ
cooky451

3
คุณช่วยอธิบายให้ฉันฟังได้ไหมว่าอะไรคือแรงจูงใจในการใช้ CRTP ในตัวอย่างนี้ ถ้าจะกำหนด bar เป็นเทมเพลต <class T> void bar (T & obj) {obj.foo (); } ถ้าอย่างนั้นคลาสใดก็ได้ที่ให้ foo ก็จะดี จากตัวอย่างของคุณดูเหมือนว่าการใช้ CRTP เพียงอย่างเดียวคือการระบุอินเทอร์เฟซในเวลาคอมไพล์ นั่นมันมีไว้เพื่ออะไร?
Anton Daneyko

1
@Dean Michael แน่นอนโค้ดในตัวอย่างคอมไพล์แม้ว่า foo จะไม่ได้กำหนดไว้ใน my_type และ your_type หากไม่มีการแทนที่ฐานเหล่านี้ :: foo จะถูกเรียกซ้ำ (และ stackoverflows) บางทีคุณอาจต้องการแก้ไขคำตอบที่คุณ cooky451 แสดง?
Anton Daneyko

@mezhaka: ใช่ตัวอย่างของ Dean Michael ไม่สมบูรณ์เพราะสามารถนำไปใช้งานได้อย่างรัดกุมยิ่งขึ้นโดยไม่ต้องใช้ CRTP อย่างที่คุณแสดง แต่เพิ่มtemplate<class T> bar(base2<T> &obj) { obj.quux(); }- คือคลาสฐานที่สองด้วยการbar()ใช้งานที่แตกต่างกันและยูทิลิตี้ของ CRTP จะปรากฏ
Nemo

18

ฉันกำลังมองหาการสนทนาที่ดีเกี่ยวกับ CRTP ด้วยตัวเอง เทคนิคของ Todd Veldhuizen สำหรับ Scientific C ++เป็นแหล่งข้อมูลที่ยอดเยี่ยมสำหรับสิ่งนี้ (1.3) และเทคนิคขั้นสูงอื่น ๆ อีกมากมายเช่นเทมเพลตนิพจน์

นอกจากนี้ฉันพบว่าคุณสามารถอ่านบทความ C ++ Gems ดั้งเดิมของ Coplien ได้ที่ Google หนังสือ อาจจะยังคงเป็นเช่นนั้น


@fizzer ฉันได้อ่านส่วนที่คุณแนะนำแล้ว แต่ยังไม่เข้าใจว่าเทมเพลต <class T_leaftype> ผลรวมสองเท่า (Matrix <T_leaftype> & A) คืออะไร ซื้อคุณเทียบกับเทมเพลต <class Whatever> ผลรวมสองเท่า (Whatever & A);
Anton Daneyko

@AntonDaneyko เมื่อเรียกบนอินสแตนซ์ฐานผลรวมของคลาสฐานจะถูกเรียกเช่น "พื้นที่ของรูปทรง" โดยใช้ค่าเริ่มต้นราวกับว่ามันเป็นสี่เหลี่ยมจัตุรัส เป้าหมายของ CRTP ในกรณีนี้คือการแก้ไขการนำไปใช้งานที่ได้รับมากที่สุด "พื้นที่ของรูปสี่เหลี่ยมคางหมู" เป็นต้นในขณะที่ยังสามารถอ้างถึงรูปสี่เหลี่ยมคางหมูเป็นรูปร่างได้จนกว่าจะต้องมีพฤติกรรมที่ได้รับมา โดยทั่วไปเมื่อใดก็ตามที่คุณต้องการตามปกติdynamic_castหรือวิธีการเสมือน
John P

1

ผมมองขึ้นCRTP มีการกระทำที่ แต่ผมพบว่าสิ่งบางอย่างเกี่ยวกับการคงความแตกต่าง ฉันสงสัยว่านี่คือคำตอบสำหรับคำถามของคุณ

ปรากฎว่าATLใช้รูปแบบนี้อย่างกว้างขวาง


-5

คำตอบ Wikipedia นี้มีทุกอย่างที่คุณต้องการ ได้แก่ :

template <class Derived> struct Base
{
    void interface()
    {
        // ...
        static_cast<Derived*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        Derived::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

แม้ว่าฉันไม่รู้ว่าสิ่งนี้ซื้อคุณได้มากแค่ไหน ค่าใช้จ่ายของการเรียกฟังก์ชันเสมือนคือ (แน่นอนขึ้นอยู่กับคอมไพเลอร์):

  • หน่วยความจำ: ตัวชี้ฟังก์ชันหนึ่งตัวต่อฟังก์ชันเสมือนจริง
  • รันไทม์: เรียกใช้ตัวชี้ฟังก์ชันเดียว

ในขณะที่ค่าโสหุ้ยของความแตกต่างแบบคงที่ของ CRTP คือ:

  • หน่วยความจำ: การทำสำเนาฐานต่อการสร้างอินสแตนซ์เทมเพลต
  • รันไทม์: เรียกใช้ตัวชี้ฟังก์ชันหนึ่งตัว + สิ่งที่ static_cast กำลังทำอยู่

4
อันที่จริงการทำซ้ำของ Base ต่อการสร้างอินสแตนซ์เทมเพลตเป็นภาพลวงตาเพราะ (เว้นแต่คุณจะยังมี vtable) คอมไพเลอร์จะรวมหน่วยเก็บข้อมูลของฐานและสิ่งที่ได้รับมาเป็นโครงสร้างเดียวสำหรับคุณ การเรียกใช้ตัวชี้ฟังก์ชันยังได้รับการปรับให้เหมาะสมโดยคอมไพลเลอร์ (ส่วน static_cast)
Dean Michael

19
อย่างไรก็ตามการวิเคราะห์ CRTP ของคุณไม่ถูกต้อง มันควรจะเป็น: ความทรงจำ: ไม่มีอะไรอย่างที่ Dean Michael พูด รันไทม์: การเรียกใช้ฟังก์ชันแบบคงที่ (เร็วกว่า) หนึ่งครั้งไม่ใช่เสมือนซึ่งเป็นจุดรวมของการออกกำลังกาย static_cast ไม่ได้ทำอะไรเลยเพียงแค่อนุญาตให้คอมไพล์โค้ด
Frederik Slijkerman

2
ประเด็นของฉันคือรหัสพื้นฐานจะซ้ำกันในอินสแตนซ์เทมเพลตทั้งหมด (การผสานรวมที่คุณพูดถึง) อคินมีเทมเพลตด้วยวิธีการเดียวที่อาศัยพารามิเตอร์เทมเพลตเท่านั้น อย่างอื่นดีกว่าในคลาสพื้นฐานมิฉะนั้นจะถูกดึงเข้า ('ผสาน') หลายครั้ง
user23167

1
แต่ละวิธีในฐานจะถูกรวบรวมอีกครั้งสำหรับแต่ละวิธีที่ได้รับ ในกรณี (คาดว่า) ที่แต่ละวิธีการสร้างอินสแตนซ์แตกต่างกัน (เนื่องจากคุณสมบัติของ Derived แตกต่างกัน) ไม่จำเป็นต้องนับเป็นค่าโสหุ้ย แต่สามารถนำไปสู่ขนาดโค้ดโดยรวมที่ใหญ่ขึ้นเทียบกับสถานการณ์ที่เมธอดที่ซับซ้อนในคลาสพื้นฐาน (ปกติ) เรียกเมธอดเสมือนของคลาสย่อย นอกจากนี้หากคุณใส่วิธียูทิลิตี้ใน Base <Derived> ซึ่งไม่ได้ขึ้นอยู่กับ <Derived> เลยพวกเขาจะยังได้รับการสร้างอินสแตนซ์ บางทีการเพิ่มประสิทธิภาพระดับโลกอาจแก้ไขได้บ้าง
greggo

การเรียกที่ผ่าน CRTP หลายชั้นจะขยายในหน่วยความจำระหว่างการคอมไพล์ แต่สามารถทำสัญญาผ่าน TCO และอินไลน์ได้อย่างง่ายดาย CRTP เองก็ไม่ได้เป็นผู้ร้ายใช่มั้ย?
John P
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.