จุดของรูปแบบ PImpl คืออะไรในขณะที่เราสามารถใช้ส่วนต่อประสานเพื่อจุดประสงค์เดียวกันใน C ++


49

ฉันเห็นซอร์สโค้ดจำนวนมากซึ่งใช้ PImpl idiom ใน C ++ ฉันถือว่ามันมีวัตถุประสงค์เพื่อซ่อนข้อมูลส่วนตัว / ประเภท / การใช้งานเพื่อให้สามารถลบการพึ่งพาและลดปัญหาการรวบรวมเวลาและส่วนหัวรวมถึงปัญหา

แต่อินเทอร์เฟซ / คลาสเพียว - นามธรรมใน C ++ ยังมีความสามารถนี้พวกเขายังสามารถใช้เพื่อซ่อนข้อมูล / ประเภท / การใช้งาน และเพื่อให้ผู้โทรเห็นส่วนต่อประสานเมื่อสร้างวัตถุเราสามารถประกาศเมธอดจากโรงงานในส่วนหัวของส่วนต่อประสาน

การเปรียบเทียบคือ:

  1. ราคา :

    ค่าใช้จ่ายในการใช้อินเตอร์เฟสลดลงเนื่องจากคุณไม่จำเป็นต้องใช้ฟังก์ชั่น wrapper สาธารณะซ้ำvoid Bar::doWork() { return m_impl->doWork(); }คุณเพียงแค่ต้องกำหนดลายเซ็นในอินเตอร์เฟส

  2. เข้าใจดี :

    เทคโนโลยีอินเทอร์เฟซเป็นที่เข้าใจกันดีกว่าโดยนักพัฒนา C ++ ทุกคน

  3. ประสิทธิภาพ :

    วิธีการเชื่อมต่อประสิทธิภาพไม่เลวร้ายยิ่งกว่าสำนวน PImpl ทั้งการเข้าถึงหน่วยความจำเพิ่มเติม ฉันถือว่าประสิทธิภาพเหมือนกัน

ต่อไปนี้เป็นรหัสเทียมเพื่อแสดงคำถามของฉัน:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

วัตถุประสงค์เดียวกันสามารถดำเนินการได้โดยใช้อินเทอร์เฟซ:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

แล้วประเด็นของ PImpl คืออะไร?

คำตอบ:


50

ประการแรก PImpl มักจะใช้สำหรับคลาสที่ไม่ใช่ polymorphic และเมื่อคลาส polymorphic มี PImpl มันมักจะยังคง polymorphic ซึ่งยังคงใช้อินเตอร์เฟสและแทนที่เมธอดเสมือนจากคลาสฐานและอื่น ๆ การใช้ PImpl ที่ง่ายกว่านั้นไม่ใช่ส่วนต่อประสาน แต่เป็นคลาสที่มีสมาชิกโดยตรงโดยตรง!

มีสามเหตุผลในการใช้ PImpl:

  1. การทำให้binary interface (ABI) เป็นอิสระจากสมาชิกส่วนตัว มีความเป็นไปได้ที่จะอัพเดตไลบรารีที่แบ่งใช้โดยไม่ต้องคอมไพล์โค้ดที่ต้องพึ่งพาซ้ำแต่ตราบใดที่อินเตอร์เฟสแบบไบนารียังคงเหมือนเดิม ตอนนี้การเปลี่ยนแปลงในส่วนหัวเกือบทั้งหมดยกเว้นการเพิ่มฟังก์ชั่นที่ไม่ใช่สมาชิกและการเพิ่มฟังก์ชั่นที่ไม่ใช่สมาชิกเสมือนเปลี่ยน ABI PImpl สำนวนย้ายคำจำกัดความของสมาชิกส่วนตัวลงในแหล่งที่มาและทำให้แตก ABI จากคำจำกัดความของพวกเขา ดูที่ปัญหาอินเตอร์เฟซแบบไบนารีที่บอบบาง

  2. เมื่อส่วนหัวมีการเปลี่ยนแปลงแหล่งที่มาทั้งหมดรวมถึงจะต้องมีการคอมไพล์ใหม่ และการคอมไพล์ C ++ นั้นค่อนข้างช้า ดังนั้นโดยการย้ายคำจำกัดความของสมาชิกส่วนตัวลงในแหล่งที่มาสำนวน PImpl จะช่วยลดเวลาในการรวบรวมเนื่องจากต้องดึงการอ้างอิงที่น้อยลงในส่วนหัวและลดเวลาการรวบรวมหลังจากการปรับเปลี่ยนมากขึ้นเนื่องจากผู้ติดตามไม่จำเป็นต้องทำการคอมไพล์ใหม่ (ตกลงนี่ใช้กับฟังก์ชั่นอินเตอร์เฟส + จากโรงงานพร้อมกับคลาสคอนกรีตที่ซ่อนอยู่ด้วย)

  3. สำหรับหลาย ๆ คลาสใน C ++ exception safety เป็นคุณสมบัติที่สำคัญ บ่อยครั้งที่คุณต้องเขียนหลายคลาสในหนึ่งเดียวดังนั้นหากในระหว่างการดำเนินการกับสมาชิกมากกว่าหนึ่งโยนไม่มีสมาชิกที่มีการแก้ไขหรือคุณมีการดำเนินการที่จะทำให้สมาชิกอยู่ในสถานะที่ไม่สอดคล้องกันถ้ามันโยนและคุณต้องการวัตถุที่มีอยู่ คงเส้นคงวา ในกรณีเช่นนี้คุณจะใช้การดำเนินการโดยสร้างอินสแตนซ์ใหม่ของ PImpl และสลับไปมาเมื่อการดำเนินการสำเร็จ

อินเทอร์เฟซที่จริงยังสามารถใช้สำหรับการซ่อนการใช้งานเท่านั้น แต่มีข้อเสียดังต่อไปนี้:

  1. การเพิ่มวิธีที่ไม่ใช่แบบเสมือนจะไม่ทำลาย ABI แต่การเพิ่มวิธีเสมือนจะไม่ทำลาย อินเทอร์เฟซจึงไม่อนุญาตให้เพิ่มวิธีเลย PImpl ทำ

  2. Inteface สามารถใช้ได้ผ่านตัวชี้ / การอ้างอิงเท่านั้นดังนั้นผู้ใช้จะต้องดูแลการจัดการทรัพยากรที่เหมาะสม ในอีกทางหนึ่งคลาสที่ใช้ PImpl ยังคงเป็นประเภทของค่าและจัดการทรัพยากรภายใน

  3. การใช้งานที่ซ่อนไม่สามารถสืบทอดได้, คลาสที่มี PImpl สามารถ

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


จุดดี. เพิ่มฟังก์ชั่นของประชาชนในImplระดับจะไม่ทำลายแต่เพิ่มฟังก์ชั่นของประชาชนในอินเตอร์เฟซที่จะทำลายABI ABI
ZijingWu

1
การเพิ่มฟังก์ชั่นสาธารณะ / วิธีการไม่ต้องทำลาย ABI; การเปลี่ยนแปลงที่เพิ่มเข้ามาอย่างเคร่งครัดไม่ได้เป็นการทำลายการเปลี่ยนแปลง อย่างไรก็ตามคุณต้องระวังอย่างมาก รหัสที่อยู่ตรงกลางระหว่าง front-end และ back-end ต้องจัดการกับความสนุกทุกประเภทด้วยการกำหนดเวอร์ชัน ทุกอย่างจะยุ่งยาก (สิ่งนี้เป็นเหตุผลว่าทำไมโครงการซอฟต์แวร์จำนวนมากถึงชอบ C ถึง C ++ ซึ่งช่วยให้พวกเขาได้รับข้อมูลที่แม่นยำยิ่งขึ้นในสิ่งที่ ABI เป็น)
Donal Fellows

3
@DonalFellows: การเพิ่มฟังก์ชั่นสาธารณะ / วิธีการไม่ทำลาย ABI การเพิ่มวิธีเสมือนทำและทำเช่นนั้นเสมอเว้นแต่ผู้ใช้จะได้รับการป้องกันไม่ให้สืบทอดวัตถุ (ซึ่ง PImpl บรรลุผลสำเร็จ)
Jan Hudec

4
เพื่อชี้แจงว่าทำไมการเพิ่มวิธีเสมือนทำให้ ABI แตก มันเกี่ยวกับการเชื่อมโยง การเชื่อมโยงไปยังวิธีการที่ไม่ใช่เสมือนเป็นชื่อโดยไม่คำนึงถึงว่าห้องสมุดเป็นแบบคงที่หรือแบบไดนามิก การเพิ่มวิธีอื่นที่ไม่ใช่แบบเสมือนเป็นการเพิ่มชื่ออื่นเพื่อลิงก์ อย่างไรก็ตามวิธีเสมือนเป็นดัชนีใน vtable และรหัสภายนอกเชื่อมโยงได้อย่างมีประสิทธิภาพโดยดัชนี การเพิ่มเมธอดเสมือนสามารถย้ายเมธอดอื่น ๆ ใน vtable โดยไม่ต้องใช้วิธีอื่นเพื่อให้รู้รหัสภายนอก
SnakE

1
PImpl ยังช่วยลดเวลารวบรวมเริ่มต้น ตัวอย่างเช่นถ้าการดำเนินงานของฉันมีข้อมูลบางประเภทแม่แบบ (เช่นunordered_set) โดยไม่มี PImpl ฉันต้องใน#include <unordered_set> MyClass.hด้วย PImpl ผมเพียงต้องการที่จะรวมไว้ใน.cppไฟล์ไม่ได้.hไฟล์และดังนั้นทุกสิ่งทุกอย่างที่มีมัน ...
มาร์ตินมาร์ตินซี

7

ฉันแค่ต้องการพูดถึงประเด็นประสิทธิภาพของคุณ หากคุณใช้อินเทอร์เฟซคุณจำเป็นต้องสร้างฟังก์ชั่นเสมือนจริงซึ่งจะไม่ถูก inline โดยเครื่องมือเพิ่มประสิทธิภาพของคอมไพเลอร์ ฟังก์ชั่น PIMPL สามารถ (และอาจจะเพราะพวกเขาสั้นมาก) จะถูกแทรก (อาจมากกว่าหนึ่งครั้งถ้าฟังก์ชั่น IMPL ยังมีขนาดเล็ก) การเรียกใช้ฟังก์ชันเสมือนไม่สามารถปรับให้เหมาะสมได้เว้นแต่คุณจะใช้การเพิ่มประสิทธิภาพการวิเคราะห์โปรแกรมแบบเต็มซึ่งใช้เวลานานมาก

หากคลาส PIMPL ของคุณไม่ได้ใช้ในวิธีการปฏิบัติงานที่สำคัญจุดนี้จะไม่สำคัญมากนัก แต่ข้อสันนิษฐานของคุณว่าประสิทธิภาพจะเหมือนกันในบางสถานการณ์เท่านั้นไม่ใช่ทั้งหมด


1
ด้วยการใช้การเพิ่มประสิทธิภาพลิงค์เวลาทำให้ค่าใช้จ่ายส่วนใหญ่ของการใช้ pimpl idiom ถูกลบออก ทั้งฟังก์ชั่นการห่อหุ้ม outter เช่นเดียวกับฟังก์ชั่นการใช้งานสามารถ inline ทำให้ฟังก์ชั่นการโทรได้อย่างมีประสิทธิภาพเช่นคลาสปกตินำมาใช้ในส่วนหัว
โกจิ

สิ่งนี้เป็นจริงเฉพาะในกรณีที่คุณใช้การเพิ่มประสิทธิภาพเวลาเชื่อมโยง จุดสำคัญของ PImpl คือคอมไพเลอร์ไม่มีการนำไปใช้แม้แต่ในการส่งต่อฟังก์ชัน ตัวเชื่อมโยงทำ
Martin C. Martin

5

หน้านี้ตอบคำถามของคุณ หลายคนเห็นด้วยกับการยืนยันของคุณ การใช้ Pimpl มีข้อดีดังต่อไปนี้เหนือเขตข้อมูล / สมาชิกส่วนบุคคล

  1. การเปลี่ยนตัวแปรสมาชิกส่วนตัวของคลาสไม่จำเป็นต้องมีการคอมไพล์คลาสใหม่ซึ่งขึ้นอยู่กับมันดังนั้นทำให้เวลาเร็วขึ้นและFragileBinaryInterfaceProblemจะลดลง
  2. ไฟล์ส่วนหัวไม่จำเป็นต้อง #include ชั้นเรียนที่ใช้ 'โดยค่า' ในตัวแปรสมาชิกส่วนตัวจึงรวบรวมเวลาได้เร็วขึ้น

Pimpl idiom เป็นการรวบรวมเวลาที่เหมาะสมซึ่งเป็นเทคนิคการแบ่งการพึ่งพา มันลดไฟล์ส่วนหัวขนาดใหญ่ มันลดส่วนหัวของคุณเป็นเพียงส่วนต่อประสานสาธารณะ ความยาวส่วนหัวมีความสำคัญใน C ++ เมื่อคุณคิดว่ามันทำงานอย่างไร #include ประสานไฟล์ส่วนหัวเข้ากับไฟล์ต้นฉบับได้อย่างมีประสิทธิภาพดังนั้นโปรดระลึกไว้ว่าหลังจากหน่วย C ++ ที่ประมวลผลล่วงหน้าอาจมีขนาดใหญ่มาก การใช้ Pimpl สามารถปรับปรุงเวลารวบรวม

มีเทคนิคการทำลายการพึ่งพาที่ดีกว่าอื่น ๆ ที่เกี่ยวข้องกับการปรับปรุงการออกแบบของคุณ วิธีการส่วนตัวแสดงถึงอะไร? ความรับผิดชอบเดียวคืออะไร? พวกเขาควรเป็นชั้นเรียนอื่นหรือไม่


PImpl ไม่ใช่การเพิ่มประสิทธิภาพในส่วนที่เกี่ยวกับเวลาทำงานเลย การจัดสรรไม่ใช่เรื่องไม่สำคัญและ pimpl หมายถึงการจัดสรรเพิ่มเติม มันเป็นเทคนิคทำลายการพึ่งพาและการเพิ่มประสิทธิภาพของการรวบรวมเวลาเท่านั้น
Jan Hudec

@JanHudec ชี้แจง
Dave Hillier

@DaveHillier, +1 สำหรับการพูดถึง FragileBinaryInterfaceProblem
ZijingWu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.