Điểm của mẫu PImpl là gì trong khi chúng ta có thể sử dụng giao diện cho cùng mục đích trong C ++?


49

Tôi thấy rất nhiều mã nguồn sử dụng thành ngữ PImpl trong C ++. Tôi giả sử mục đích của nó là để ẩn dữ liệu / loại / triển khai riêng tư, vì vậy nó có thể loại bỏ sự phụ thuộc và sau đó giảm thời gian biên dịch và tiêu đề bao gồm vấn đề.

Nhưng các lớp giao diện / trừu tượng thuần túy trong C ++ cũng có khả năng này, chúng cũng có thể được sử dụng để ẩn dữ liệu / kiểu / triển khai. Và để cho người gọi chỉ nhìn thấy giao diện khi tạo một đối tượng, chúng ta có thể khai báo một phương thức xuất xưởng trong tiêu đề của giao diện.

Sự so sánh là:

  1. Chi phí :

    Chi phí cách giao diện thấp hơn, bởi vì bạn thậm chí không cần lặp lại việc thực hiện chức năng trình bao bọc công khai void Bar::doWork() { return m_impl->doWork(); }, bạn chỉ cần xác định chữ ký trong giao diện.

  2. Hiểu rõ :

    Công nghệ giao diện được hiểu rõ hơn bởi mọi nhà phát triển C ++.

  3. Hiệu suất :

    Hiệu suất cách giao diện không tệ hơn thành ngữ PImpl, cả truy cập bộ nhớ bổ sung. Tôi giả sử hiệu suất là như nhau.

Sau đây là mã giả để minh họa câu hỏi của tôi:

// 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;
};

Mục đích tương tự có thể được thực hiện bằng giao diện:

// 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();

Vậy quan điểm của PImpl là gì?

Câu trả lời:


50

Đầu tiên, PImpl thường được sử dụng cho các lớp không đa hình. Và khi một lớp đa hình có PImpl, nó thường vẫn là đa hình, đó vẫn là các giao diện và ghi đè các phương thức ảo từ lớp cơ sở, v.v. Vì vậy, việc thực hiện PImpl đơn giản hơn không phải là giao diện, nó là một lớp đơn giản trực tiếp chứa các thành viên!

Có ba lý do để sử dụng PImpl:

  1. Làm cho giao diện nhị phân (ABI) độc lập với các thành viên tư nhân. Có thể cập nhật thư viện dùng chung mà không cần biên dịch lại mã phụ thuộc, nhưng chỉ khi giao diện nhị phân vẫn giữ nguyên. Bây giờ hầu như bất kỳ thay đổi nào trong tiêu đề, ngoại trừ việc thêm chức năng không phải thành viên và thêm chức năng thành viên không ảo, sẽ thay đổi ABI. Thành ngữ PImpl chuyển định nghĩa của các thành viên tư nhân vào nguồn và do đó tách ABI khỏi định nghĩa của chúng. Xem vấn đề giao diện nhị phân dễ vỡ

  2. Khi một tiêu đề thay đổi, tất cả các nguồn bao gồm nó phải được biên dịch lại. Và quá trình biên dịch C ++ khá chậm. Vì vậy, bằng cách di chuyển các định nghĩa của các thành viên riêng vào nguồn, thành ngữ PImpl làm giảm thời gian biên dịch, vì cần ít phụ thuộc hơn trong tiêu đề và giảm thời gian biên dịch sau khi sửa đổi nhiều hơn vì người phụ thuộc không cần phải biên dịch lại (ok, điều này áp dụng cho giao diện + chức năng nhà máy với lớp bê tông ẩn).

  3. Đối với nhiều lớp trong C ++, an toàn ngoại lệ là một thuộc tính quan trọng. Thường thì bạn cần phải soạn nhiều lớp thành một để nếu trong quá trình hoạt động trên nhiều thành viên ném, không có thành viên nào bị sửa đổi hoặc bạn có thao tác sẽ khiến thành viên ở trạng thái không nhất quán nếu nó ném và bạn cần giữ đối tượng chứa thích hợp. Trong trường hợp như vậy, bạn thực hiện thao tác bằng cách tạo phiên bản mới của PImpl và trao đổi chúng khi hoạt động thành công.

Trên thực tế, giao diện cũng có thể được sử dụng để ẩn thực hiện, nhưng có những nhược điểm sau:

  1. Thêm phương thức không ảo không phá vỡ ABI, nhưng thêm phương thức ảo thì không. Do đó, các giao diện không cho phép thêm các phương thức, PImpl cũng vậy.

  2. Inteface chỉ có thể được sử dụng thông qua con trỏ / tham chiếu, vì vậy người dùng phải chăm sóc quản lý tài nguyên thích hợp. Mặt khác, các lớp sử dụng PImpl vẫn là các loại giá trị và xử lý các tài nguyên bên trong.

  3. Việc triển khai ẩn không thể được kế thừa, lớp có PImpl.

Và tất nhiên giao diện sẽ không giúp với sự an toàn ngoại lệ. Bạn cần sự quyết định trong lớp cho điều đó.


Điểm tốt. Thêm chức năng công khai trong Impllớp sẽ không phá vỡ ABI, nhưng thêm chức năng công cộng trong giao diện sẽ phá vỡ ABI.
ZijingWu

1
Thêm một chức năng / phương thức công khai không phải phá vỡ ABI; thay đổi nghiêm ngặt phụ gia không phá vỡ thay đổi. Tuy nhiên, bạn phải luôn cẩn thận; mã trung gian giữa front-end và back-end phải đối phó với tất cả các loại thú vị với phiên bản. Tất cả đều trở nên khó khăn. (Điều này loại điều là lý do tại sao một số dự án phần mềm thích C đến C ++, nó cho phép họ có được một va li chặt chẽ hơn về chính xác những gì ABI là.)
Donal Fellows

3
@DonalFellows: Thêm một chức năng / phương thức công khai không phá vỡ ABI. Việc thêm một phương thức ảo sẽ thực hiện và luôn luôn như vậy trừ khi người dùng bị ngăn không cho kế thừa đối tượng (mà PImpl đạt được).
Jan Hudec

4
Để làm rõ lý do tại sao việc thêm một phương thức ảo phá vỡ ABI. Nó phải làm với liên kết. Liên kết đến các phương thức không ảo là theo tên, bất kể thư viện là tĩnh hay động. Thêm một phương thức không ảo khác chỉ là thêm một tên khác để liên kết. Tuy nhiên, các phương thức ảo là các chỉ mục trong một vtable và mã bên ngoài liên kết hiệu quả theo chỉ mục. Thêm một phương thức ảo có thể di chuyển các phương thức khác xung quanh trong vtable mà không có cách nào để mã bên ngoài biết.
SnakE 18/03/2016

1
PImpl cũng làm giảm thời gian biên dịch ban đầu. Ví dụ, nếu thực hiện của tôi có một trường của một số mẫu loại (ví dụ unordered_set), mà không pImpl, tôi cần phải #include <unordered_set>MyClass.h. Với PImpl, tôi chỉ cần đưa nó vào .cpptệp chứ không phải .htệp và do đó mọi thứ khác bao gồm nó ...
Martin C. Martin

7

Tôi chỉ muốn giải quyết điểm hiệu suất của bạn. Nếu bạn sử dụng một giao diện, bạn nhất thiết phải tạo các hàm ảo SILL KHÔNG được tối ưu hóa bởi trình tối ưu hóa của trình biên dịch. Các hàm PIMPL có thể (và có thể sẽ, vì chúng quá ngắn) được nội tuyến (có thể nhiều hơn một lần nếu hàm IMPL cũng nhỏ). Một cuộc gọi chức năng ảo không thể được tối ưu hóa trừ khi bạn sử dụng tối ưu hóa phân tích chương trình đầy đủ mà phải mất một thời gian rất dài để thực hiện.

Nếu lớp PIMPL của bạn không được sử dụng theo cách thức hiệu suất, thì điểm này không quan trọng lắm, nhưng giả định của bạn rằng hiệu suất là như nhau chỉ giữ trong một số tình huống, không phải tất cả.


1
Với việc sử dụng tối ưu hóa thời gian liên kết, phần lớn chi phí sử dụng thành ngữ pimpl được loại bỏ. Cả hai hàm bao bọc bên ngoài cũng như các hàm thực hiện đều có thể được nội tuyến, làm cho các lệnh gọi hàm có hiệu quả giống như một lớp bình thường được triển khai bên trong một tiêu đề.
goji

Điều này chỉ đúng nếu bạn sử dụng tối ưu hóa thời gian liên kết. Điểm của PImpl là trình biên dịch không có triển khai, thậm chí không có chức năng chuyển tiếp. Các liên kết làm mặc dù.
Martin C. Martin

5

Trang này trả lời câu hỏi của bạn. Nhiều người đồng ý với khẳng định của bạn. Sử dụng Pimpl có những ưu điểm sau so với các lĩnh vực / thành viên tư nhân.

  1. Việc thay đổi các biến thành viên riêng của một lớp không yêu cầu biên dịch lại các lớp phụ thuộc vào nó, do đó làm cho thời gian nhanh hơn và FragileBinaryInterfacePro Hiệu bị giảm.
  2. Tệp tiêu đề không cần #include các lớp được sử dụng 'theo giá trị' trong các biến thành viên riêng, do đó thời gian biên dịch nhanh hơn.

Thành ngữ Pimpl là một tối ưu hóa thời gian biên dịch, một kỹ thuật phá vỡ phụ thuộc. Nó cắt giảm các tập tin tiêu đề lớn. Nó giảm tiêu đề của bạn xuống chỉ giao diện công cộng. Độ dài tiêu đề rất quan trọng trong C ++ khi bạn nghĩ về cách thức hoạt động của nó. #include kết hợp hiệu quả tệp tiêu đề với tệp nguồn - vì vậy, hãy nhớ rằng sau khi các đơn vị C ++ được xử lý trước có thể rất lớn. Sử dụng Pimpl có thể cải thiện thời gian biên dịch.

Có những kỹ thuật phá vỡ sự phụ thuộc tốt hơn khác - liên quan đến việc cải thiện thiết kế của bạn. Các phương thức riêng đại diện cho cái gì? Trách nhiệm duy nhất là gì? Họ có nên là một lớp khác?


PImpl hoàn toàn không phải là một sự tối ưu hóa trong sự tôn trọng thời gian. Phân bổ không tầm thường và pimpl có nghĩa là phân bổ thêm. Đây là một kỹ thuật phá vỡ phụ thuộc và chỉ tối ưu hóa thời gian biên dịch .
Jan Hudec

@JanHudec làm rõ.
Dave Hillier

@DaveHillier, +1 để đề cập đến FragileBinaryInterfaceProbols
ZijingWu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.