Là thành ngữ pImpl thực sự được sử dụng trong thực tế?


165

Tôi đang đọc cuốn sách "C ++ đặc biệt" của Herb Sutter, và trong cuốn sách đó tôi đã tìm hiểu về thành ngữ pImpl. Về cơ bản, ý tưởng là tạo ra một cấu trúc cho các privateđối tượng của a classvà phân bổ động chúng để giảm thời gian biên dịch (và cũng ẩn các cài đặt riêng theo cách tốt hơn).

Ví dụ:

class X
{
private:
  C c;
  D d;  
} ;

có thể được thay đổi thành:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

và, trong CPP, định nghĩa:

struct X::XImpl
{
  C c;
  D d;
};

Điều này có vẻ khá thú vị, nhưng tôi chưa bao giờ thấy cách tiếp cận này trước đây, trong các công ty tôi đã làm việc, cũng như trong các dự án nguồn mở mà tôi đã thấy mã nguồn. Vì vậy, tôi tự hỏi nó kỹ thuật này thực sự được sử dụng trong thực tế?

Tôi nên sử dụng nó ở khắp mọi nơi, hoặc thận trọng? Và kỹ thuật này có được khuyến nghị sử dụng trong các hệ thống nhúng (trong đó hiệu suất rất quan trọng)?


Đây có phải là cơ bản giống như một quyết định rằng X là một giao diện (trừu tượng) và Ximpl là thực hiện? struct XImpl : public X. Điều đó cảm thấy tự nhiên hơn đối với tôi. Có một số vấn đề khác tôi đã bỏ lỡ?
Aaron McDaid

@AaronMcDaid: Tương tự, nhưng có những ưu điểm là (a) các hàm thành viên không phải là ảo và (b) bạn không cần một nhà máy, hoặc định nghĩa của lớp triển khai, để khởi tạo nó.
Mike Seymour

2
@AaronMcDaid Thành ngữ pimpl tránh các cuộc gọi chức năng ảo. Đó cũng là một chút C ++ - ish (đối với một số quan niệm về C ++ - ish); bạn gọi các nhà xây dựng, thay vì các chức năng của nhà máy. Tôi đã sử dụng cả hai, tùy thuộc vào những gì có trong cơ sở mã hiện có --- thành ngữ pimpl (ban đầu được gọi là thành ngữ mèo Cheshire và dự đoán mô tả của Herb về nó ít nhất 5 năm) dường như có lịch sử lâu hơn và nhiều hơn Được sử dụng rộng rãi trong C ++, nhưng nếu không, cả hai đều hoạt động.
James Kanze

30
Trong C ++, pimpl nên được thực hiện với const unique_ptr<XImpl>chứ không phảiXImpl* .
Neil G

1
"Chưa bao giờ thấy cách tiếp cận này trước đây, trong các công ty tôi đã làm việc, cũng như trong các dự án nguồn mở". Qt hầu như không bao giờ sử dụng nó KHÔNG.
ManuelSchneid3r

Câu trả lời:


132

Vì vậy, tôi tự hỏi nó kỹ thuật này thực sự được sử dụng trong thực tế? Tôi nên sử dụng nó ở khắp mọi nơi, hoặc thận trọng?

Tất nhiên nó được sử dụng. Tôi sử dụng nó trong dự án của tôi, trong hầu hết các lớp.


Lý do sử dụng thành ngữ PIMPL:

Tương thích nhị phân

Khi bạn đang phát triển thư viện, bạn có thể thêm / sửa đổi các trường thành XImpl mà không phá vỡ tính tương thích nhị phân với máy khách của bạn (điều đó có nghĩa là sự cố!). Vì bố cục nhị phân của Xlớp không thay đổi khi bạn thêm các trường mới vào Ximpllớp, nên việc thêm chức năng mới vào thư viện trong các bản cập nhật phiên bản nhỏ là an toàn.

Tất nhiên, bạn cũng có thể thêm các phương thức không ảo công khai / riêng tư mới vào X / XImplmà không phá vỡ tính tương thích nhị phân, nhưng đó là ngang bằng với kỹ thuật thực hiện / tiêu đề tiêu chuẩn.

Ẩn dữ liệu

Nếu bạn đang phát triển một thư viện, đặc biệt là một thư viện độc quyền, có thể bạn không muốn tiết lộ những thư viện / kỹ thuật triển khai nào khác được sử dụng để triển khai giao diện chung của thư viện của bạn. Vì vấn đề Sở hữu trí tuệ hoặc vì bạn tin rằng người dùng có thể bị đưa ra các giả định nguy hiểm về việc triển khai hoặc chỉ phá vỡ sự đóng gói bằng cách sử dụng các thủ thuật đúc khủng khiếp. PIMPL giải quyết / giảm thiểu điều đó.

Thời gian biên soạn

Thời gian biên dịch được giảm xuống, vì chỉ Xcần xây dựng lại tệp nguồn (triển khai) khi bạn thêm / xóa các trường và / hoặc phương thức vào XImpllớp (ánh xạ để thêm các trường / phương thức riêng tư trong kỹ thuật tiêu chuẩn). Trong thực tế, đó là một hoạt động phổ biến.

Với kỹ thuật triển khai / tiêu đề tiêu chuẩn (không có PIMPL), khi bạn thêm một trường mới vào X, mọi máy khách đã từng phân bổ X(trên ngăn xếp hoặc trên heap) cần phải được biên dịch lại, bởi vì nó phải điều chỉnh kích thước của phân bổ. Chà, mọi khách hàng chưa từng phân bổ X cũng cần được biên dịch lại, nhưng đó chỉ là chi phí hoạt động (mã kết quả ở phía máy khách sẽ giống nhau).

Hơn nữa, với việc phân tách tiêu đề / triển khai tiêu chuẩn XClient1.cppcần phải được biên dịch lại ngay cả khi một phương thức riêng X::foo()được thêm vào XX.hthay đổi, mặc dù XClient1.cppkhông thể gọi phương thức này vì lý do đóng gói! Giống như ở trên, đó là chi phí hoạt động thuần túy và có liên quan đến cách các hệ thống xây dựng C ++ ngoài đời thực hoạt động.

Tất nhiên, việc biên dịch lại là không cần thiết khi bạn chỉ sửa đổi việc triển khai các phương thức (vì bạn không chạm vào tiêu đề), nhưng đó là ngang bằng với kỹ thuật thực hiện / tiêu đề tiêu chuẩn.


Có phải kỹ thuật này được khuyến nghị sử dụng trong các hệ thống nhúng (trong đó hiệu suất rất quan trọng)?

Điều đó phụ thuộc vào mức độ mạnh mẽ của mục tiêu của bạn. Tuy nhiên, câu trả lời duy nhất cho câu hỏi này là: đo lường và đánh giá những gì bạn được và mất. Ngoài ra, hãy xem xét rằng nếu bạn không xuất bản một thư viện có nghĩa là được sử dụng trong các hệ thống nhúng bởi các khách hàng của bạn, chỉ áp dụng lợi thế về thời gian biên dịch!


16
+1 vì nó được sử dụng rộng rãi trong công ty tôi cũng làm việc và vì những lý do tương tự.
Benoit

9
Ngoài ra, khả năng tương thích nhị phân
Ambroz Bizjak

9
Trong thư viện Qt, phương thức này cũng được sử dụng trong các tình huống con trỏ thông minh. Vì vậy, QString giữ nội dung của nó như là một lớp bất biến trong nội bộ. Khi lớp công khai được "sao chép", con trỏ của thành viên riêng được sao chép thay vì toàn bộ lớp riêng. Các lớp riêng này sau đó cũng sử dụng các con trỏ thông minh, do đó về cơ bản bạn có được bộ sưu tập rác với hầu hết các lớp, ngoài hiệu năng được cải thiện đáng kể do sao chép con trỏ thay vì sao chép toàn bộ lớp
Timothy Baldridge

8
Thậm chí, với thành ngữ pimpl Qt có thể duy trì cả khả năng tương thích nhị phân tiến và lùi trong một phiên bản chính duy nhất (trong hầu hết các trường hợp). IMO đây là lý do quan trọng nhất để sử dụng nó.
Whitequark

1
Nó cũng hữu ích để triển khai mã dành riêng cho nền tảng, vì bạn có thể giữ lại cùng một API.
doc

49

Có vẻ như rất nhiều thư viện ngoài kia sử dụng nó để ổn định trong API của họ, ít nhất là đối với một số phiên bản.

Nhưng đối với tất cả mọi thứ, bạn không bao giờ nên sử dụng bất cứ điều gì ở khắp mọi nơi mà không cần thận trọng. Luôn suy nghĩ trước khi sử dụng nó. Đánh giá những lợi thế mà nó mang lại cho bạn, và nếu chúng xứng đáng với giá bạn phải trả.

Những lợi thế nó có thể mang lại cho bạn là:

  • giúp duy trì khả năng tương thích nhị phân của các thư viện dùng chung
  • ẩn một số chi tiết nội bộ
  • giảm chu kỳ biên dịch lại

Những điều đó có thể hoặc không thể là lợi thế thực sự cho bạn. Giống như tôi, tôi không quan tâm đến thời gian biên dịch lại vài phút. Người dùng cuối cũng thường không, vì họ luôn biên dịch nó một lần và từ đầu.

Những bất lợi có thể là (cũng ở đây, tùy thuộc vào việc thực hiện và liệu chúng có phải là nhược điểm thực sự cho bạn không):

  • Tăng mức sử dụng bộ nhớ do phân bổ nhiều hơn so với biến thể ngây thơ
  • tăng nỗ lực bảo trì (bạn phải viết ít nhất các chức năng chuyển tiếp)
  • mất hiệu năng (trình biên dịch có thể không có khả năng nội tuyến như với một triển khai ngây thơ của lớp của bạn)

Vì vậy, cẩn thận cung cấp cho tất cả mọi thứ một giá trị, và đánh giá nó cho chính mình. Đối với tôi, hầu như luôn luôn chỉ ra rằng việc sử dụng thành ngữ pimpl không đáng để bỏ công sức. Chỉ có một trường hợp cá nhân tôi sử dụng nó (hoặc ít nhất là một cái gì đó tương tự):

Trình bao bọc C ++ của tôi cho statcuộc gọi linux . Ở đây, cấu trúc từ tiêu đề C có thể khác nhau, tùy thuộc vào những gì #definesđược đặt. Và vì tiêu đề trình bao bọc của tôi không thể kiểm soát tất cả chúng, tôi chỉ #include <sys/stat.h>trong .cxxtệp của mình và tránh những vấn đề này.


2
Nó hầu như luôn luôn được sử dụng cho các giao diện hệ thống, để làm cho hệ thống mã giao diện độc lập. Ví dụ, Filelớp của tôi (hiển thị nhiều thông tin statsẽ trả về trong Unix) sử dụng cùng một giao diện trong cả Windows và Unix.
James Kanze

5
@JamesKanze: Ngay cả ở đó, cá nhân tôi trước tiên sẽ ngồi một lát và suy nghĩ xem liệu có thể không đủ để có một vài #ifdefgiây để làm cho lớp bọc càng mỏng càng tốt. Nhưng mỗi người có những mục tiêu khác nhau, điều quan trọng là dành thời gian để suy nghĩ về nó thay vì mù quáng làm theo một cái gì đó.
PlasmaHH

31

Đồng ý với tất cả những người khác về hàng hóa, nhưng hãy để tôi đưa ra bằng chứng giới hạn: không hoạt động tốt với các mẫu .

Lý do là việc khởi tạo mẫu yêu cầu khai báo đầy đủ có sẵn trong đó việc khởi tạo diễn ra. (Và đó là lý do chính khiến bạn không thấy các phương thức mẫu được xác định trong tệp CPP)

Bạn vẫn có thể tham khảo các lớp con templetised, nhưng vì bạn phải bao gồm tất cả chúng, mọi lợi thế của "tách rời triển khai" khi biên dịch (tránh bao gồm tất cả mã cụ thể của platoform ở mọi nơi, rút ​​ngắn quá trình biên dịch).

Là một mô hình tốt cho OOP cổ điển (dựa trên kế thừa) nhưng không phải cho lập trình chung (dựa trên chuyên môn hóa).


4
Bạn phải chính xác hơn: hoàn toàn không có vấn đề gì khi sử dụng các lớp PIMPL làm đối số kiểu mẫu. Chỉ khi bản thân lớp triển khai cần được tham số hóa trên các đối số khuôn mẫu của lớp bên ngoài, nó không thể bị ẩn khỏi tiêu đề giao diện nữa, ngay cả khi nó vẫn là một lớp riêng. Nếu bạn có thể xóa đối số mẫu, bạn chắc chắn vẫn có thể thực hiện PIMPL "đúng". Với việc xóa kiểu, bạn cũng có thể thực hiện PIMPL trong một lớp không phải là mẫu cơ sở, và sau đó có lớp mẫu xuất phát từ nó.
Phục hồi Monica

22

Những người khác đã cung cấp các nhược điểm kỹ thuật, nhưng tôi nghĩ những điều sau đây đáng chú ý:

Đầu tiên và quan trọng nhất, đừng giáo điều. Nếu pImpl hoạt động cho tình huống của bạn, hãy sử dụng nó - đừng sử dụng nó chỉ vì "nó tốt hơn OO vì nó thực sự che giấu việc thực hiện", v.v. Trích dẫn Câu hỏi thường gặp về C ++:

đóng gói là cho mã, không phải người ( nguồn )

Chỉ để cung cấp cho bạn một ví dụ về phần mềm nguồn mở nơi nó được sử dụng và tại sao: OpenThreads, thư viện luồng được sử dụng bởi OpenSceneGraph . Ý tưởng chính là loại bỏ khỏi tiêu đề (ví dụ <Thread.h>) tất cả mã dành riêng cho nền tảng, bởi vì các biến trạng thái bên trong (ví dụ xử lý luồng) khác nhau từ nền tảng đến nền tảng. Bằng cách này, người ta có thể biên dịch mã theo thư viện của bạn mà không có bất kỳ kiến ​​thức nào về các đặc điểm riêng của nền tảng khác, bởi vì mọi thứ đều bị ẩn.


12

Tôi chủ yếu sẽ xem xét PIMPL cho các lớp tiếp xúc được sử dụng làm API bởi các mô-đun khác. Điều này có nhiều lợi ích, vì nó làm cho việc biên dịch lại các thay đổi được thực hiện trong triển khai PIMPL không ảnh hưởng đến phần còn lại của dự án. Ngoài ra, đối với các lớp API, chúng thúc đẩy khả năng tương thích nhị phân (các thay đổi trong triển khai mô-đun không ảnh hưởng đến các máy khách của các mô-đun đó, chúng không phải được biên dịch lại vì triển khai mới có cùng giao diện nhị phân - giao diện được PIMPL trưng ra).

Đối với việc sử dụng PIMPL cho mỗi lớp, tôi sẽ cân nhắc thận trọng vì tất cả những lợi ích đó đều phải trả giá: cần thêm một mức độ gián tiếp để truy cập các phương thức triển khai.


"một mức độ bổ sung được yêu cầu để truy cập các phương thức thực hiện." Nó là?
xaxxon

@xaxxon vâng, đúng vậy. pimpl chậm hơn nếu các phương pháp ở mức thấp. không bao giờ sử dụng nó cho những thứ sống trong một vòng lặp chặt chẽ, ví dụ.
Erik Aronesty

@xaxxon Tôi muốn nói trong trường hợp chung là cần có thêm cấp độ. Nếu nội tuyến được thực hiện hơn không. Nhưng inlinning sẽ không phải là một tùy chọn trong mã được biên dịch trong một dll khác.
Ghita

5

Tôi nghĩ rằng đây là một trong những công cụ cơ bản nhất để tách rời.

Tôi đã sử dụng pimpl (và nhiều thành ngữ khác từ C ++ đặc biệt) trong dự án nhúng (SetTopBox).

Mục đích cụ thể của idoim này trong dự án của chúng tôi là để ẩn các kiểu sử dụng lớp XImpl. Cụ thể, chúng tôi đã sử dụng nó để ẩn chi tiết triển khai cho các phần cứng khác nhau, trong đó các tiêu đề khác nhau sẽ được đưa vào. Chúng tôi đã triển khai các lớp XImpl khác nhau cho một nền tảng và khác nhau cho nền tảng khác. Bố cục của lớp X vẫn như cũ bất kể platfrom.


4

Tôi đã từng sử dụng kỹ thuật này rất nhiều trong quá khứ nhưng sau đó tôi thấy mình đang rời xa nó.

Tất nhiên đó là một ý tưởng tốt để ẩn chi tiết triển khai khỏi người dùng của lớp bạn. Tuy nhiên, bạn cũng có thể làm điều đó bằng cách cho người dùng của lớp sử dụng một giao diện trừu tượng và để chi tiết triển khai trở thành lớp cụ thể.

Ưu điểm của pImpl là:

  1. Giả sử chỉ có một triển khai giao diện này, thì rõ ràng hơn bằng cách không sử dụng lớp trừu tượng / triển khai cụ thể

  2. Nếu bạn có một bộ các lớp (một mô-đun) sao cho một số lớp truy cập vào cùng một "impl" nhưng người dùng mô-đun sẽ chỉ sử dụng các lớp "bị lộ".

  3. Không có bảng v nếu điều này được coi là một điều xấu.

Những nhược điểm tôi tìm thấy của pImpl (nơi giao diện trừu tượng hoạt động tốt hơn)

  1. Mặc dù bạn có thể chỉ có một triển khai "sản xuất", bằng cách sử dụng giao diện trừu tượng, bạn cũng có thể tạo ra một "giả" thực hiện hoạt động trong thử nghiệm đơn vị.

  2. (Vấn đề lớn nhất). Trước những ngày unique_ptr và di chuyển, bạn đã hạn chế các lựa chọn về cách lưu trữ pImpl. Một con trỏ thô và bạn có vấn đề về lớp của bạn không thể sao chép. Một auto_ptr cũ sẽ không hoạt động với lớp được khai báo chuyển tiếp (không phải trên tất cả các trình biên dịch). Vì vậy, mọi người bắt đầu sử dụng shared_ptr, điều này rất tốt trong việc làm cho lớp của bạn có thể sao chép được nhưng tất nhiên cả hai bản sao đều có chung shared_ptr mà bạn có thể không mong đợi (sửa đổi một và cả hai đều được sửa đổi). Vì vậy, giải pháp thường là sử dụng con trỏ thô cho bên trong và làm cho lớp không thể sao chép và trả về shared_ptr cho điều đó thay vào đó. Thế là hai cuộc gọi mới. (Trên thực tế 3 đã chia sẻ cũ_ptr đã cho bạn một cái thứ hai).

  3. Về mặt kỹ thuật không thực sự chính xác vì hằng số không được truyền qua một con trỏ thành viên.

Nói chung, do đó tôi đã chuyển đi trong nhiều năm từ pImpl và thay vào đó là sử dụng giao diện trừu tượng (và các phương thức xuất xưởng để tạo các thể hiện).


3

Như nhiều người khác đã nói, thành ngữ Pimpl cho phép đạt được tính độc lập ẩn và biên dịch thông tin đầy đủ, không may với chi phí mất hiệu năng (bổ sung con trỏ bổ sung) và nhu cầu bộ nhớ bổ sung (chính con trỏ thành viên). Chi phí bổ sung có thể rất quan trọng trong việc phát triển phần mềm nhúng, đặc biệt là trong các tình huống mà bộ nhớ phải được tiết kiệm càng nhiều càng tốt. Sử dụng các lớp trừu tượng C ++ làm giao diện sẽ dẫn đến cùng một lợi ích với cùng một chi phí. Điều này thực sự cho thấy sự thiếu hụt lớn của C ++, trong đó, không lặp lại với các giao diện giống như C (phương thức toàn cầu với con trỏ mờ làm tham số), không thể ẩn thông tin thực sự và độc lập biên dịch mà không có nhược điểm tài nguyên bổ sung: điều này chủ yếu là do khai báo một lớp, phải được bao gồm bởi người dùng của nó,


3

Đây là một kịch bản thực tế tôi gặp phải, nơi thành ngữ này đã giúp rất nhiều. Gần đây tôi đã quyết định hỗ trợ DirectX 11, cũng như hỗ trợ DirectX 9 hiện có của tôi, trong một công cụ trò chơi. Công cụ đã bao bọc hầu hết các tính năng DX, vì vậy không có giao diện DX nào được sử dụng trực tiếp; họ chỉ được định nghĩa trong các tiêu đề là thành viên tư nhân. Công cụ này sử dụng DLL làm tiện ích mở rộng, thêm bàn phím, chuột, cần điều khiển và hỗ trợ tập lệnh, như tuần như nhiều tiện ích mở rộng khác. Mặc dù hầu hết các DLL đó không sử dụng DX trực tiếp, nhưng chúng đòi hỏi kiến ​​thức và liên kết với DX đơn giản chỉ vì chúng kéo theo các tiêu đề tiếp xúc với DX. Khi thêm DX 11, sự phức tạp này đã tăng lên đáng kể, tuy nhiên không cần thiết. Di chuyển các thành viên DX vào một Pimpl chỉ được xác định trong nguồn đã loại bỏ sự áp đặt này. Trên hết việc giảm phụ thuộc thư viện này,


2

Nó được sử dụng trong thực tế trong rất nhiều dự án. Đó là sự hữu ích phụ thuộc rất nhiều vào loại dự án. Một trong những dự án nổi bật hơn sử dụng điều này là Qt , trong đó ý tưởng cơ bản là ẩn mã triển khai hoặc mã nền tảng khỏi người dùng (các nhà phát triển khác sử dụng Qt).

Đây là một ý tưởng cao quý nhưng có một nhược điểm thực sự: gỡ lỗi Miễn là mã ẩn trong ẩn riêng có chất lượng cao thì tất cả đều ổn, nhưng nếu có lỗi trong đó, thì người dùng / nhà phát triển có vấn đề, bởi vì nó chỉ là một con trỏ câm cho một triển khai ẩn, ngay cả khi anh ta có mã nguồn triển khai.

Vì vậy, trong gần như tất cả các quyết định thiết kế có một ưu và nhược điểm.


9
thật ngu ngốc nhưng nó đã gõ ... tại sao bạn không thể theo mã trong trình gỡ lỗi?
ChúZeiv

2
Nói chung, để gỡ lỗi thành mã Qt, bạn cần tự xây dựng Qt. Khi bạn thực hiện, không có vấn đề gì khi bước vào các phương pháp PIMPL và kiểm tra nội dung của dữ liệu PIMPL.
Phục hồi Monica

0

Một lợi ích tôi có thể thấy là nó cho phép lập trình viên thực hiện một số thao tác nhất định theo cách khá nhanh:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Tôi hy vọng tôi không hiểu nhầm ngữ nghĩa di chuyển.

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.