Mẫu cho giao diện an toàn trong C ++ là gì


22

Lưu ý: sau đây là mã C ++ 03, nhưng chúng tôi hy vọng sẽ chuyển sang C ++ 11 trong hai năm tới, vì vậy chúng tôi phải ghi nhớ điều đó.

Tôi đang viết một hướng dẫn (cho người mới, trong số những người khác) về cách viết giao diện trừu tượng trong C ++. Tôi đã đọc cả hai bài viết của Sutter về chủ đề này, tìm kiếm trên internet các ví dụ và câu trả lời, và thực hiện một số bài kiểm tra.

Mã này KHÔNG được biên dịch!

void foo(SomeInterface & a, SomeInterface & b)
{
   SomeInterface c ;               // must not be default-constructible
   SomeInterface d(a);             // must not be copy-constructible
   a = b ;                         // must not be assignable
}

Tất cả các hành vi trên đều tìm ra nguồn gốc của vấn đề của chúng khi cắt : Giao diện trừu tượng (hoặc lớp không có lá trong cấu trúc phân cấp) không nên có thể xây dựng cũng như không thể chuyển đổi / gán được, NGAY nếu lớp dẫn xuất có thể.

Giải pháp thứ 0: giao diện cơ bản

class VirtuallyDestructible
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

Giải pháp này rất đơn giản và hơi ngây thơ: Nó không thực hiện được tất cả các ràng buộc của chúng tôi: Nó có thể được xây dựng mặc định, xây dựng sao chép và gán sao chép (Tôi thậm chí không chắc chắn về các nhà xây dựng di chuyển và chuyển nhượng, nhưng tôi vẫn còn 2 năm để hình dung nó ra).

  1. Chúng tôi không thể khai báo công cụ ảo thuần túy bởi vì chúng tôi cần giữ cho nó nội tuyến và một số trình biên dịch của chúng tôi sẽ không tiêu hóa các phương thức ảo thuần túy với phần thân trống nội tuyến.
  2. Vâng, điểm duy nhất của lớp này là làm cho những người thực hiện hầu như bị phá hủy, đó là một trường hợp hiếm gặp.
  3. Ngay cả khi chúng ta có một phương thức thuần ảo bổ sung (phần lớn các trường hợp), lớp này vẫn có thể được sao chép.

Vì vậy, không ...

Giải pháp thứ nhất: boost :: không thể quét

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

Giải pháp này là tốt nhất, vì nó đơn giản, rõ ràng và C ++ (không có macro)

Vấn đề là nó vẫn không hoạt động cho giao diện cụ thể đó bởi vì VirtualConstructible vẫn có thể được xây dựng mặc định .

  1. Chúng tôi không thể khai báo công cụ ảo thuần vì chúng tôi cần giữ nội tuyến và một số trình biên dịch của chúng tôi sẽ không tiêu hóa được.
  2. Vâng, điểm duy nhất của lớp này là làm cho những người thực hiện hầu như bị phá hủy, đó là một trường hợp hiếm gặp.

Một vấn đề khác là các lớp thực hiện giao diện không thể sao chép sau đó phải khai báo / xác định rõ ràng hàm tạo sao chép và toán tử gán nếu chúng cần có các phương thức đó (và trong mã của chúng tôi, chúng tôi có các lớp giá trị vẫn có thể được truy cập bởi khách hàng của chúng tôi thông qua giao diện).

Điều này đi ngược lại Quy tắc không, đó là nơi chúng ta muốn đến: Nếu việc triển khai mặc định là ổn, thì chúng ta sẽ có thể sử dụng nó.

Giải pháp thứ 2: làm cho chúng được bảo vệ!

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      // With C++11, these methods would be "= default"
      MyInterface() {}
      MyInterface(const MyInterface & ) {}
      MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;

Mẫu này tuân theo các ràng buộc kỹ thuật mà chúng tôi đã có (ít nhất là trong mã người dùng): MyInterface không thể được xây dựng mặc định, không thể được xây dựng sao chép và không thể được gán bản sao.

Ngoài ra, nó không áp đặt các ràng buộc giả tạo nào trong việc triển khai các lớp , sau đó có thể tự do tuân theo Quy tắc 0 hoặc thậm chí khai báo một số hàm tạo / toán tử là "= default" trong C ++ 11/14 mà không gặp vấn đề gì.

Bây giờ, điều này khá dài dòng và một giải pháp thay thế sẽ là sử dụng macro, đại loại như:

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;

Việc bảo vệ phải nằm ngoài macro (vì nó không có phạm vi).

Chính xác là "không gian tên" (nghĩa là tiền tố với tên của công ty hoặc sản phẩm của bạn), macro sẽ vô hại.

Và lợi thế là mã được bao gồm trong một nguồn, thay vì được sao chép dán trong tất cả các giao diện. Nếu công cụ xây dựng di chuyển và chuyển nhượng di chuyển bị vô hiệu hóa rõ ràng theo cùng một cách trong tương lai, đây sẽ là một thay đổi rất nhẹ trong mã.

Phần kết luận

  • Tôi có bị hoang tưởng muốn mã được bảo vệ chống cắt trong giao diện không? (Tôi tin là tôi không, nhưng người ta không bao giờ biết ...)
  • Giải pháp tốt nhất trong số những người ở trên là gì?
  • Có cách nào khác, giải pháp tốt hơn?

Xin nhớ rằng đây là một mô hình sẽ đóng vai trò là kim chỉ nam cho người mới (trong số những người khác), vì vậy một giải pháp như: "Mỗi trường hợp nên thực hiện nó" không phải là một giải pháp khả thi.

Tiền thưởng và kết quả

Tôi đã trao tiền thưởng cho coredump vì thời gian dành cho việc trả lời các câu hỏi và sự liên quan của các câu trả lời.

Giải pháp của tôi cho vấn đề có thể sẽ đi đến một cái gì đó như thế:

class MyInterface
{
   DECLARE_CLASS_AS_INTERFACE(MyInterface) ;

   public :
      // the virtual methods
} ;

... với macro sau:

#define DECLARE_CLASS_AS_INTERFACE(ClassName)                                \
   public :                                                                  \
      virtual ~ClassName() {}                                                \
   protected :                                                               \
      ClassName() {}                                                         \
      ClassName(const ClassName & ) {}                                       \
      ClassName & operator = (const ClassName & ) { return *this ; }         \
   private :

Đây là một giải pháp khả thi cho vấn đề của tôi vì những lý do sau:

  • Lớp này không thể được khởi tạo (các hàm tạo được bảo vệ)
  • Lớp này hầu như có thể bị phá hủy
  • Lớp này có thể được kế thừa mà không áp đặt các ràng buộc không đáng có đối với các lớp kế thừa (ví dụ: lớp kế thừa có thể được mặc định là có thể sao chép được)
  • Việc sử dụng macro có nghĩa là "khai báo" giao diện có thể dễ dàng nhận ra (và có thể tìm kiếm) và mã của nó được đặt ở một nơi giúp dễ dàng sửa đổi hơn (một tên tiền tố phù hợp sẽ loại bỏ xung đột tên không mong muốn)

Lưu ý rằng các câu trả lời khác đã đưa ra cái nhìn sâu sắc có giá trị. Cảm ơn tất cả các bạn đã cho nó một shot.

Lưu ý rằng tôi đoán tôi vẫn có thể đặt một tiền thưởng khác cho câu hỏi này và tôi đánh giá cao những câu trả lời khai sáng đủ để tôi thấy một câu hỏi, tôi sẽ mở một tiền thưởng chỉ để gán câu trả lời đó.


5
Bạn có thể đơn giản sử dụng các chức năng ảo thuần túy trong giao diện không? virtual void bar() = 0;ví dụ? Điều đó sẽ ngăn giao diện của bạn không bị ảnh hưởng.
Morwenn

@Morwenn: Như đã nói trong câu hỏi, điều đó sẽ giải quyết 99% các trường hợp (tôi nhắm tới 100% nếu có thể). Ngay cả khi chúng tôi chọn bỏ qua 1% còn thiếu, nó cũng sẽ không giải quyết được việc cắt bài tập. Vì vậy, không, đây không phải là một giải pháp tốt.
paercebal

@Morwenn: Nghiêm túc chứ? ... :-D ... Lần đầu tiên tôi viết câu hỏi này trên StackOverflow, và sau đó thay đổi suy nghĩ của tôi ngay trước khi gửi nó. Bạn có tin rằng tôi nên xóa nó ở đây, và gửi nó cho SO?
paercebal

Nếu tôi đúng, tất cả những gì bạn cần là virtual ~VirtuallyDestructible() = 0và kế thừa ảo các lớp giao diện (chỉ với các thành viên trừu tượng). Bạn có thể bỏ qua VirtualDestestable, có khả năng.
Dieter Lücking

5
@paercebal: Nếu trình biên dịch sặc trên các lớp ảo thuần thì nó thuộc về thùng rác. Một giao diện thực là theo định nghĩa thuần ảo.
Không ai vào

Câu trả lời:


13

Cách chính tắc để tạo giao diện trong C ++ là cung cấp cho nó một hàm hủy ảo thuần túy. Điều này đảm bảo rằng

  • Không có phiên bản nào của lớp giao diện có thể được tạo, bởi vì C ++ không cho phép bạn tạo một thể hiện của một lớp trừu tượng. Điều này quan tâm đến các yêu cầu không thể xây dựng (cả mặc định và sao chép).
  • Gọi deletemột con trỏ tới giao diện thực hiện đúng: nó gọi hàm hủy của lớp có nguồn gốc nhất cho trường hợp đó.

chỉ cần có một hàm hủy ảo thuần túy sẽ không ngăn chặn việc gán tham chiếu đến giao diện. Nếu bạn cũng muốn điều đó thất bại, thì bạn phải thêm một toán tử gán được bảo vệ vào giao diện của bạn.

Bất kỳ trình biên dịch C ++ nào cũng có thể xử lý một lớp / giao diện như thế này (tất cả trong một tệp tiêu đề):

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

Nếu bạn có một trình biên dịch cuộn cảm về điều này (có nghĩa là nó phải là tiền C ++ 98), thì tùy chọn 2 của bạn (có các hàm tạo được bảo vệ) là tốt nhất thứ hai.

Việc sử dụng boost::noncopyablekhông được khuyến khích cho nhiệm vụ này, vì nó gửi thông điệp rằng tất cả các lớp trong hệ thống phân cấp sẽ không thể sao chép và do đó có thể gây nhầm lẫn cho các nhà phát triển có kinh nghiệm hơn, những người không quen với ý định sử dụng nó như thế này.


If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.: Đây là gốc rễ của vấn đề của tôi. Các trường hợp tôi cần một giao diện để hỗ trợ gán phải thực sự hiếm. Mặt khác, các trường hợp tôi muốn chuyển giao diện bằng tham chiếu (các trường hợp NULL không được chấp nhận), và do đó, muốn tránh việc không biên dịch hoặc cắt mà biên dịch lớn hơn nhiều.
paercebal

Là toán tử gán không bao giờ được gọi, tại sao bạn lại định nghĩa nó? Như một bên, tại sao không làm cho nó private? Ngoài ra, bạn có thể muốn xử lý mặc định- và copy-ctor.
Ded repeatator

5

Tôi có bị hoang tưởng không ...

  • Tôi có bị hoang tưởng muốn mã được bảo vệ chống cắt trong giao diện không? (Tôi tin là tôi không, nhưng người ta không bao giờ biết ...)

Đây không phải là một vấn đề quản lý rủi ro?

  • Bạn có sợ rằng một lỗi liên quan đến cắt lát có khả năng được giới thiệu?
  • Bạn có nghĩ rằng nó có thể không được chú ý và gây ra lỗi không thể phục hồi?
  • đến mức độ nào bạn sẵn sàng để tránh cắt lát?

Giải pháp tốt nhất

  • Giải pháp tốt nhất trong số những người ở trên là gì?

Giải pháp thứ hai của bạn ("làm cho chúng được bảo vệ") có vẻ tốt, nhưng hãy nhớ rằng tôi không phải là chuyên gia về C ++.
Ít nhất, các tập quán không hợp lệ dường như được báo cáo chính xác là sai bởi trình biên dịch của tôi (g ++).

Bây giờ, bạn có cần macro không? Tôi sẽ nói "có", bởi vì mặc dù bạn không nói mục đích của hướng dẫn bạn đang viết là gì, tôi đoán điều này là để thực thi một tập hợp thực tiễn tốt nhất trong mã sản phẩm của bạn.

Với mục đích đó, các macro có thể giúp phát hiện khi mọi người áp dụng hiệu quả mẫu: một bộ lọc cam kết cơ bản có thể cho bạn biết liệu macro có được sử dụng hay không:

  • nếu được sử dụng, thì mẫu có khả năng được áp dụng và quan trọng hơn là được áp dụng chính xác (chỉ cần kiểm tra xem có protectedtừ khóa không),
  • nếu không được sử dụng, bạn có thể thử điều tra lý do tại sao nó không được sử dụng.

Không có macro, bạn phải kiểm tra xem mô hình có cần thiết và được triển khai tốt trong mọi trường hợp hay không.

Giải pháp tốt hơn

  • Có cách nào khác, giải pháp tốt hơn?

Cắt lát trong C ++ không gì khác hơn là một đặc thù của ngôn ngữ. Vì bạn đang viết một hướng dẫn (đặc biệt cho người mới), bạn nên tập trung vào việc giảng dạy và không chỉ liệt kê "quy tắc mã hóa". Bạn phải chắc chắn rằng bạn thực sự giải thích cách thức và lý do cắt lát xảy ra, cùng với các ví dụ và bài tập (không phát minh lại bánh xe, lấy cảm hứng từ sách và hướng dẫn).

Ví dụ, tiêu đề của một bài tập có thể là " Mẫu cho giao diện an toàn trong C ++ là gì?"

Vì vậy, động thái tốt nhất của bạn sẽ là đảm bảo rằng các nhà phát triển C ++ của bạn hiểu những gì đang xảy ra khi cắt lát xảy ra. Tôi tin rằng nếu họ làm như vậy, họ sẽ không mắc nhiều lỗi trong mã như bạn sợ, ngay cả khi không chính thức thực thi mẫu cụ thể đó (nhưng bạn vẫn có thể thực thi nó, cảnh báo trình biên dịch là tốt).

Về trình biên dịch

Bạn nói :

Tôi không có quyền lựa chọn trình biên dịch cho sản phẩm này,

Thông thường mọi người sẽ nói "Tôi không có quyền làm [X]" , "Tôi không được phép làm [Y] ..." , ... bởi vì họ nghĩ rằng điều này là không thể, và không phải vì họ đã thử hoặc hỏi.

Nó có thể là một phần của mô tả công việc của bạn để đưa ra ý kiến ​​của bạn về các vấn đề kỹ thuật; nếu bạn thực sự nghĩ rằng trình biên dịch là sự lựa chọn hoàn hảo (hoặc duy nhất) cho miền vấn đề của bạn, thì hãy sử dụng nó. Nhưng bạn cũng đã nói "các công cụ hủy diệt ảo thuần túy với việc triển khai nội tuyến không phải là điểm nghẹt thở tồi tệ nhất tôi từng thấy" ; Theo hiểu biết của tôi, trình biên dịch đặc biệt đến nỗi ngay cả những người phát triển C ++ có kiến ​​thức cũng gặp khó khăn khi sử dụng nó: trình biên dịch / di sản nội bộ của bạn hiện là nợ kỹ thuật và bạn có quyền (nhiệm vụ?) để thảo luận vấn đề đó với các nhà phát triển và quản lý khác .

Cố gắng đánh giá chi phí giữ trình biên dịch so với chi phí sử dụng trình biên dịch khác:

  1. Trình biên dịch hiện tại mang đến cho bạn điều gì mà không ai khác có thể làm được?
  2. Mã sản phẩm của bạn có dễ dàng biên dịch bằng trình biên dịch khác không? Tại sao không ?

Tôi không biết tình huống của bạn và trên thực tế bạn có thể có những lý do hợp lệ để gắn với một trình biên dịch cụ thể.
Nhưng trong trường hợp đây chỉ là quán tính đơn giản, tình huống sẽ không bao giờ phát triển nếu bạn hoặc đồng nghiệp của bạn không báo cáo các vấn đề về năng suất hoặc nợ công nghệ.


Am I paranoid...: "Làm cho giao diện của bạn dễ sử dụng một cách chính xác và khó sử dụng không chính xác". Tôi đã nếm thử nguyên tắc đặc biệt đó khi ai đó báo cáo một trong những phương thức tĩnh của tôi, do nhầm lẫn, được sử dụng không chính xác. Lỗi được tạo ra dường như không liên quan và phải mất nhiều giờ để một kỹ sư tìm ra nguồn. "Lỗi giao diện" này ngang bằng với việc gán tham chiếu giao diện cho người khác. Vì vậy, vâng, tôi muốn tránh loại lỗi đó. Ngoài ra, trong C ++, triết lý là bắt càng nhiều càng tốt vào thời gian biên dịch và ngôn ngữ mang lại cho chúng ta sức mạnh đó, vì vậy chúng tôi đi theo nó.
paercebal

Best solution: Tôi đồng ý. . . Better solution: Đó là một câu trả lời tuyệt vời. Tôi sẽ làm việc với nó ... Bây giờ, về Pure virtual classes: Cái gì đây? Một giao diện trừu tượng C ++? (lớp không có trạng thái và chỉ có phương thức ảo thuần túy?). Làm thế nào "lớp ảo thuần" này bảo vệ tôi khỏi bị cắt? (các phương thức ảo thuần túy sẽ làm cho việc khởi tạo không được biên dịch, nhưng chuyển nhượng sao chép và chuyển nhượng cũng sẽ chuyển IIrc).
paercebal

About the compiler: Chúng tôi đồng ý, nhưng trình biên dịch của chúng tôi nằm ngoài phạm vi trách nhiệm của tôi (không phải điều đó ngăn tôi khỏi những bình luận lén lút ... :-p ...). Tôi sẽ không tiết lộ chi tiết (tôi ước tôi có thể) nhưng nó được gắn với các lý do bên trong (như bộ thử nghiệm) và lý do bên ngoài (ví dụ: máy khách liên kết với thư viện của chúng tôi). Cuối cùng, thay đổi phiên bản trình biên dịch (hoặc thậm chí vá nó) KHÔNG phải là một hoạt động tầm thường. Hãy để một mình thay thế một trình biên dịch bị hỏng bằng một gcc gần đây.
paercebal

@paercebal cảm ơn ý kiến ​​của bạn; về các lớp ảo thuần túy, bạn nói đúng, nó không giải quyết được tất cả các ràng buộc của bạn (tôi sẽ loại bỏ phần này). Tôi hiểu phần "lỗi giao diện" và cách bắt lỗi tại thời gian biên dịch là hữu ích: nhưng bạn đã hỏi liệu bạn có bị hoang tưởng không và tôi nghĩ cách tiếp cận hợp lý là để cân bằng nhu cầu kiểm tra tĩnh của bạn với khả năng xảy ra lỗi. Chúc may mắn với điều biên dịch :)
coredump

1
Tôi không phải là một fan hâm mộ của các macro, đặc biệt là vì các hướng dẫn được nhắm mục tiêu (cũng) ở nam giới. Quá thường xuyên, tôi đã thấy những người được cung cấp những công cụ tiện dụng như thế này để áp dụng chúng một cách mù quáng và không bao giờ hiểu chuyện gì đang thực sự xảy ra. Họ tin rằng những gì vĩ mô làm phải là điều phức tạp nhất bởi vì ông chủ của họ nghĩ rằng sẽ quá khó để họ tự làm. Và bởi vì macro chỉ tồn tại trong công ty của bạn, họ thậm chí không thể thực hiện tìm kiếm trên web trong khi hướng dẫn được ghi thành tài liệu về chức năng thành viên nào để khai báo và tại sao, họ có thể.
5gon12eder

2

Vấn đề cắt lát là một, nhưng chắc chắn không phải là vấn đề duy nhất, được giới thiệu khi bạn hiển thị giao diện đa hình thời gian chạy cho người dùng của bạn. Hãy nghĩ về con trỏ null, quản lý bộ nhớ, dữ liệu chia sẻ. Không ai trong số này dễ dàng giải quyết trong mọi trường hợp (con trỏ thông minh là tuyệt vời, nhưng thậm chí chúng không phải là viên đạn bạc). Trên thực tế, từ bài đăng của bạn, có vẻ như bạn đang cố gắng giải quyết vấn đề cắt lát, nhưng lại bỏ qua nó bằng cách không cho phép người dùng tạo bản sao. Tất cả những gì bạn cần làm để đưa ra giải pháp cho vấn đề cắt lát, là thêm chức năng thành viên nhân bản ảo. Tôi nghĩ vấn đề sâu xa hơn với việc phơi bày một giao diện đa hình thời gian chạy là bạn buộc người dùng phải đối phó với ngữ nghĩa tham chiếu, khó lý luận hơn so với ngữ nghĩa giá trị.

Cách tốt nhất mà tôi biết để tránh những vấn đề này trong C ++ là sử dụng kiểu xóa . Đây là một kỹ thuật trong đó bạn ẩn một giao diện đa hình thời gian chạy, đằng sau một giao diện lớp bình thường. Giao diện lớp bình thường này sau đó có ngữ nghĩa giá trị và chăm sóc tất cả các 'mớ hỗn độn' đa hình phía sau màn hình. std::functionlà một ví dụ điển hình của loại tẩy.

Đối với một lời giải thích tuyệt vời về lý do tại sao phơi bày quyền thừa kế cho người dùng của bạn là xấu và cách xóa loại có thể giúp khắc phục xem các bản trình bày này của Sean Parent:

Kế thừa là Lớp cơ sở của Ác ma (phiên bản ngắn)

Giá trị ngữ nghĩa và đa hình dựa trên khái niệm (phiên bản dài; dễ theo dõi hơn, nhưng âm thanh không hay)


0

Bạn không hoang tưởng. Nhiệm vụ chuyên nghiệp đầu tiên của tôi với tư cách là một lập trình viên C ++ đã dẫn đến việc cắt và sụp đổ. Tôi biết những người khác. Không có nhiều giải pháp tốt cho việc này.

Với các ràng buộc trình biên dịch của bạn, tùy chọn 2 là tốt nhất. Thay vì tạo một macro, mà các lập trình viên mới của bạn sẽ xem là kỳ lạ và bí ẩn, tôi sẽ đề xuất một tập lệnh hoặc công cụ để tự động tạo mã. Nếu nhân viên mới của bạn sẽ sử dụng IDE, bạn sẽ có thể tạo công cụ "Giao diện MYCOMPANY mới" sẽ hỏi tên giao diện và tạo cấu trúc bạn đang tìm kiếm.

Nếu các lập trình viên của bạn đang sử dụng dòng lệnh, thì hãy sử dụng bất kỳ ngôn ngữ kịch bản nào có sẵn cho bạn để tạo tập lệnh NewMyCompanyInterface để tạo mã.

Tôi đã sử dụng phương pháp này trong quá khứ cho các mẫu mã phổ biến (giao diện, máy trạng thái, v.v.). Phần thú vị là các lập trình viên mới có thể đọc đầu ra và hiểu nó một cách dễ dàng, sao chép mã cần thiết khi họ cần thứ gì đó không thể được tạo.

Macro và các phương pháp lập trình meta khác có xu hướng làm xáo trộn những gì đang xảy ra và các lập trình viên mới không tìm hiểu những gì đang xảy ra 'đằng sau bức màn'. Khi họ phải phá vỡ mô hình, họ vẫn mất như trước.

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.