2012-06-27 14 views
23

Tôi không thể nghĩ về một tiêu đề câu hỏi thích hợp để mô tả sự cố. Hy vọng rằng các chi tiết dưới đây giải thích rõ ràng vấn đề của tôi.Ngăn người dùng phát sinh từ cơ sở CRTP không chính xác

Xét đoạn mã sau

#include <iostream> 

template <typename Derived> 
class Base 
{ 
    public : 

    void call() 
    { 
     static_cast<Derived *>(this)->call_impl(); 
    } 
}; 

class D1 : public Base<D1> 
{ 
    public : 

    void call_impl() 
    { 
     data_ = 100; 
     std::cout << data_ << std::endl; 
    } 

    private : 

    int data_; 
}; 

class D2 : public Base<D1> // This is wrong by intension 
{ 
    public : 

    void call_impl() 
    { 
     std::cout << data_ << std::endl; 
    } 

    private : 

    int data_; 
}; 

int main() 
{ 
    D2 d2; 
    d2.call_impl(); 
    d2.call(); 
    d2.call_impl(); 
} 

Nó sẽ biên dịch và chạy mặc dù định nghĩa của D2 là cố ý sai. Cuộc gọi đầu tiên d2.call_impl() sẽ xuất ra một số bit ngẫu nhiên được mong đợi là D2::data_ không được khởi tạo. Các cuộc gọi thứ hai và thứ ba tất cả sẽ xuất ra 100 cho số data_.

Tôi hiểu lý do tại sao nó sẽ biên dịch và chạy, sửa tôi nếu tôi sai.

Khi chúng tôi thực hiện cuộc gọi d2.call(), cuộc gọi đã được giải quyết để Base<D1>::call, và điều đó sẽ đúc this để D1 và gọi D1::call_impl. Bởi vì D1 thực sự là hình thức có nguồn gốc Base<D1>, do đó, diễn viên là tốt tại thời gian biên dịch.

Vào lúc chạy, sau khi các diễn viên, this, trong khi nó đang thực sự là một đối tượng D2 được xử lý như thể nó là D1, và cuộc gọi đến D1::call_impl sẽ sửa đổi các bit nhớ được nghĩa vụ phải được D1::data_, và đầu ra. Trong trường hợp này, các bit này xảy ra ở nơi D2::data_. Tôi nghĩ rằng d2.call_impl() thứ hai cũng sẽ là hành vi không xác định tùy thuộc vào việc thực hiện C++.

Vấn đề là, mã này, trong khi sai về cơ bản, sẽ không có dấu hiệu lỗi cho người dùng. Những gì tôi đang thực sự làm trong dự án của tôi là tôi có một lớp cơ sở CRTP hoạt động như một công cụ điều phối. Một lớp khác trong thư viện truy cập giao diện lớp cơ sở CRTP, nói callcall sẽ gửi đến call_dispatch có thể là lớp thực thi mặc định cơ sở hoặc triển khai lớp dẫn xuất. Tất cả những điều này sẽ hoạt động tốt nếu người dùng định nghĩa lớp dẫn xuất, nói D, thực sự bắt nguồn từ Base<D>. Nó sẽ tăng lỗi thời gian biên dịch nếu nó có nguồn gốc từ Base<Unrelated> trong đó Unrelated không được lấy từ Base<Unrelated>. Nhưng nó sẽ không ngăn người dùng viết mã như trên.

Người dùng sử dụng thư viện này bằng cách lấy từ lớp cơ sở CRTP và cung cấp một số chi tiết triển khai. Chắc chắn có những lựa chọn thay thế thiết kế khác có thể tránh được vấn đề sử dụng không chính xác như trên (ví dụ một lớp cơ sở trừu tượng). Nhưng bây giờ hãy để chúng sang một bên và chỉ tin rằng tôi cần thiết kế này vì một lý do nào đó.

Vì vậy, câu hỏi của tôi là, có cách nào để tôi có thể ngăn người dùng viết lớp không chính xác có nguồn gốc như xem ở trên không. Tức là, nếu người dùng viết một lớp triển khai có nguồn gốc, hãy nói D, nhưng anh ta lấy nó từ Base<OtherD>, thì lỗi thời gian biên dịch sẽ được nâng lên.

Một giải pháp là sử dụng dynamic_cast. Tuy nhiên, đó là mở rộng và ngay cả khi nó hoạt động nó là một lỗi thời gian chạy.

+0

Câu trả lời ngắn gọn: không. 'dynamic_cast' có thể tốn kém, nhưng nó rẻ hơn so với cố gắng sửa người dùng của bạn. – Rook

+1

Ít nhất dynamic_cast thất bại sẽ được tìm thấy nhanh hơn trong các bài kiểm tra đơn vị. Bạn không thể có trình biên dịch bảo vệ bạn khỏi mọi lỗi có thể, như viết 'i + 1' khi có nghĩa là 'i - 1'. Điều này là tương tự. –

+2

bản sao có thể có của [Cách tránh lỗi trong khi sử dụng CRTP?] (Http://stackoverflow.com/questions/4417782/how-to-avoid-errors-while-using-crtp) –

Trả lời

32

1) làm cho tất cả nhà xây dựng của cơ sở tư nhân (nếu không có nhà xây dựng, bổ sung một)

2) tuyên bố tham số mẫu nguồn gốc như người bạn của cơ sở

template <class Derived> 
class Base 
{ 
private: 

    Base(){}; // prevent undesirable inheritance making ctor private 
    friend Derived; // allow inheritance for Derived 

public : 

    void call() 
    { 
     static_cast<Derived *>(this)->call_impl(); 
    } 
}; 

Sau này nó sẽ là không thể tạo ra bất kỳ trường hợp của D2 thừa kế sai.

+0

Tôi nghĩ rằng điều này sẽ làm việc! Tại sao những điều thực sự thông minh này luôn trở nên thực sự đơn giản! Điều này trông rất giống như một thủ thuật Barton-Nackman –

+0

@Yan Zhou: Tôi đã thử nó, có vẻ như nó hoạt động :) Tôi cũng chỉ tìm thấy một câu hỏi liên quan trên SO: http://stackoverflow.com/questions/5907731/how-to -implement-a-compile-time-check-that-a-downcast-là-hợp lệ-in-a-crtp và trả lời nó quá, nhưng, có lẽ, có một số cạm bẫy khi câu hỏi xuất hiện tổng quát hơn (không phải về thừa kế sai chỉ có). – user396672

+2

@ user396672: Lỗ hổng duy nhất tôi có thể thấy là nó bị hỏng hình thành trước C++ 11. Tiêu chuẩn không được phép có một mẫu lớp khai báo một tham số kiểu mẫu làm bạn. –

2

Điểm chung: Mẫu không được bảo vệ khỏi việc được khởi tạo với thông số sai. Đây là vấn đề nổi tiếng. Bạn không nên dành nhiều thời gian để sửa lỗi này. Số lượng hoặc cách thức các mẫu có thể bị lạm dụng là vô tận. Trong trường hợp cụ thể của bạn, bạn có thể phát minh ra một cái gì đó. Sau đó, bạn sẽ sửa đổi mã của mình và các cách lạm dụng mới sẽ hiển thị.

Tôi biết rằng C++ 11 có xác nhận tĩnh có thể hữu ích. Tôi không biết đầy đủ chi tiết.

Điểm khác. Bên cạnh các lỗi biên dịch có phân tích tĩnh. Những gì bạn đang yêu cầu có một số điều gì đó với điều này. Phân tích không nhất thiết phải tìm lỗi bảo mật. Nó có thể đảm bảo rằng không có recusrion trong mã. Nó có thể kiểm tra rằng không có các dẫn xuất từ ​​một số lớp, bạn có thể đặt ra các hạn chế về các tham số của các khuôn mẫu và các hàm, vv Đây là tất cả các phân tích. Các ràng buộc thay đổi rộng lớn như vậy không thể được trình biên dịch hỗ trợ. Tôi không chắc rằng đây là cách đi đúng đắn, chỉ nói về khả năng này.

p.s. Công ty chúng tôi cung cấp dịch vụ trong lĩnh vực này.

+1

Tôi thực sự không thể tìm thấy bất cứ điều gì ở đây mà sẽ trả lời câu hỏi của mình, chỉ khuyến mãi cho công ty của bạn ... – PlasmaHH

+0

Thôi nào. Tôi đang nói rằng: 1. C + + 2003 không được bảo vệ tốt cho việc tạo mẫu sai. 2. C++ 11 có thể hữu ích. 3. Phân tích thuật toán có thể hữu ích. Không nhất thiết với sự giúp đỡ của các công cụ của chúng tôi. Tôi đang nói rằng các công cụ khác sẽ không hoạt động ở đâu? –

+0

Anh ấy hỏi/chính xác/giúp gì. Nói rằng có điều gì đó ở đó có thể giúp đỡ nhiều như tôi đoán anh ta đã tìm ra – PlasmaHH

4

Nếu bạn có sẵn C++ 11, bạn có thể sử dụng static_assert (nếu không, tôi chắc chắn bạn có thể mô phỏng những điều này bằng cách tăng cường). Bạn có thể khẳng định ví dụ: is_convertible<Derived*,Base*> hoặc is_base_of<Base,Derived>.

Tất cả điều này diễn ra trong Cơ sở và tất cả những gì nó có là thông tin có nguồn gốc. Nó sẽ không bao giờ có cơ hội để xem ngữ cảnh gọi là từ D2 hay D1, vì điều này không tạo ra sự khác biệt, vì Base<D1> được khởi tạo một lần, theo một cách cụ thể, cho dù nó được khởi tạo bởi D1 hay D2 bắt nguồn từ nó (hoặc bởi người sử dụng một cách rõ ràng instantiating nó).

Vì bạn không muốn (dễ hiểu, vì nó có chi phí thời gian chạy đôi khi đáng kể và bộ nhớ overhead) sử dụng dynamic_cast, cố gắng sử dụng một cái gì đó thường được gọi là "nhiều diễn viên" (tăng có biến thể riêng của mình quá):

template<class R, class T> 
R poly_cast(T& t) 
{ 
#ifndef NDEBUG 
     (void)dynamic_cast<R>(t); 
#endif 
     return static_cast<R>(t); 
} 

Bằng cách này trong quá trình gỡ lỗi/thử nghiệm của bạn, hãy phát hiện lỗi được phát hiện. Mặc dù không đảm bảo 100%, trong thực tế, điều này thường bắt được tất cả những sai lầm mà mọi người mắc phải.

+1

Tôi đã có cùng một ý tưởng, nhưng điều này là không dễ dàng như nó có vẻ. Nếu bạn sử dụng nó trong phần thân của 'Base' như thế này' static_assert (is_base_of , Derived> :: value, "...") 'điều này sẽ không hoạt động, bởi vì nếu template được khởi tạo với' D1' sẽ trở thành 'static_assert (is_base_of < Base< D1 >, D1> :: value," ... ")' không kích hoạt xác nhận (sau khi tất cả 'D1' được bắt nguồn từ' Base < D1 > '). Các lỗi duy nhất này bắt là những cái bắt nguồn từ 'Base < D3 >' trong đó 'D3' không tự kế thừa từ' Base' chính xác. Tuy nhiên, các lỗi này đã được ghi lại bởi 'static_cast'. – LiKao

+0

@LiKao: ah, bây giờ tôi hiểu ý của bạn là gì, hãy để tôi thêm vào câu trả lời. – PlasmaHH

+0

Cảm ơn gợi ý nhiều diễn viên. Tôi chỉ nghĩ ra một ý tưởng rất giống nhau. Tôi khẳng định dynamic_cast trong chế độ gỡ lỗi và sau đó làm các static_cast. Các giải pháp mẫu rõ ràng cũng rất thanh lịch. –

1

Nếu bạn không thể đếm với C++ 11, bạn có thể thử mẹo này:

  1. Thêm một chức năng tĩnh trong Base mà trả về một con trỏ đến kiểu specialied của nó:

    tĩnh nguồn gốc * có nguồn gốc() {return NULL; }

  2. Thêm một tĩnh check chức năng mẫu để căn cứ mà phải mất một con trỏ:

    mẫu < typename T> static bool séc (T * derived_this) { return (derived_this == Cơ sở < nguồn gốc>: :nguồn gốc()); }

  3. Trong Dn nhà thầu của bạn, hãy gọi check(this):

    séc (này)

Bây giờ nếu bạn cố gắng để biên dịch:

$ g++ -Wall check_inherit.cpp -o check_inherit 
check_inherit.cpp: In instantiation of ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’: 
check_inherit.cpp:46:16: required from here 
check_inherit.cpp:19:62: error: comparison between distinct pointer types ‘D2*’ and ‘D1*’ lacks a cast                                
check_inherit.cpp: In static member function ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:                             
check_inherit.cpp:20:5: warning: control reaches end of non-void function [-Wreturn-type]                                   
1

Nói chung tôi không nghĩ rằng có một cách để có được điều này, mà không nên được coi là hoàn toàn xấu xí và trở lại với việc sử dụng các tính năng độc ác. Đây là một bản tóm tắt về những gì sẽ làm việc và những gì sẽ không.

  • Sử dụng static_assert (hoặc từ C++ 11 hoặc từ tăng) không hoạt động, vì một tấm séc trong định nghĩa của Base chỉ có thể sử dụng các loại Base<Derived>Derived. Vì vậy, sau đây sẽ nhìn tốt, nhưng thất bại:

    template <typename Derived> 
    class Base 
    { 
        public : 
    
        void call() 
        { 
         static_assert(sizeof(Derived) != 0 && std::is_base_of< Base<Derived>, Derived >::value, "Missuse of CRTP"); 
         static_cast<Derived *>(this)->call_impl(); 
        } 
    }; 
    

Trong trường hợp bạn cố gắng tuyên bố D2 như class D2 : Base<D1> sự khẳng định tĩnh sẽ không bắt này, vì D1 actualy có nguồn gốc từ Base<D1> và khẳng định tĩnh là hoàn toàn có hiệu lực. Tuy nhiên, nếu bạn lấy được từ Base<D3> trong đó D3 thì bất kỳ lớp nào không phát sinh từ Base<D3> cả hai static_assert cũng như static_cast sẽ kích hoạt lỗi biên dịch, vì vậy điều này hoàn toàn vô dụng.

Kể từ khi loại D2 bạn sẽ cần phải kiểm tra trong mã của Base không bao giờ được chuyển cho template cách duy nhất để sử dụng static_assert sẽ được di chuyển nó sau khi tuyên bố của D2 mà sẽ đòi hỏi cùng một người người thực hiện D2 để kiểm tra, một lần nữa là vô ích.

Một cách để làm được việc này sẽ là bằng cách thêm một vĩ mô, nhưng điều này sẽ gây ra không có gì nhưng xấu xa tinh khiết:

#define MAKE_DISPATCHABLE_BEGIN(DeRiVeD) \ 
    class DeRiVeD : Base<DeRiVed> { 
#define MAKE_DISPATCHABLE_END(DeRiVeD) 
    }; \ 
    static_assert(is_base_of< Base<Derived>, Derived >::value, "Error"); 

này chỉ tăng sự xấu xa, và static_assert là một lần nữa trở thành vô ích, bởi vì các mẫu đảm bảo rằng các loại luôn luôn phù hợp. Vì vậy, không đạt được ở đây.

  • Tùy chọn tốt nhất: Quên tất cả điều này và sử dụng dynamic_cast có ý nghĩa rõ ràng cho trường hợp này. Nếu bạn cần điều này thường xuyên hơn có lẽ sẽ có ý nghĩa khi thực hiện asserted_cast của riêng bạn (có một bài viết về Tiến sĩ Jobbs về điều này), điều này sẽ tự động kích hoạt xác nhận không thành công khi dynamic_cast không thành công.
1

Không có cách nào để ngăn người dùng viết các lớp có nguồn gốc không chính xác; tuy nhiên, có nhiều cách để ngăn chặn mã của bạn gọi các lớp có phân cấp không mong muốn. Nếu có các điểm mà tại đó người dùng đang chuyển Derived đến các chức năng của thư viện, hãy xem xét việc thực hiện các chức năng thư viện đó theo cách static_cast đối với loại có nguồn gốc dự kiến. Ví dụ:

template < typename Derived > 
void safe_call(Derived& t) 
{ 
    static_cast< Base<Derived>& >(t).call(); 
} 

Hoặc nếu có nhiều cấp thứ bậc, xem xét như sau:

template < typename Derived, 
      typename BaseArg > 
void safe_call_helper(Derived& d, 
         Base<BaseArg>& b) 
{ 
    // Verify that Derived does inherit from BaseArg. 
    static_cast< BaseArg& >(d).call(); 
} 

template < typename T > 
void safe_call(T& t) 
{ 
    safe_call_helper(t, t); 
} 

Trong cả hai trường hợp thse, safe_call(d1) sẽ biên dịch trong khi safe_call(d2) sẽ thất bại để biên dịch. Lỗi trình biên dịch có thể không rõ ràng như người ta muốn cho người dùng, vì vậy nó có thể đáng giá để xem xét các xác nhận tĩnh.

Các vấn đề liên quan