2012-03-30 34 views
13

Tôi có một lớp:mẫu để tránh dynamic_cast

class A 
{ 
public: 
    virtual void func() {...} 
    virtual void func2() {...} 
}; 

Và một số lớp học có nguồn gốc từ thế này, cho phép nói B, C, D ... Trong 95% các trường hợp, tôi muốn đi qua tất cả đối tượng và func hoặc Func2 (gọi), vì thế tôi có chúng trong một vector, như:

std::vector<std::shared_ptr<A> > myVec; 
... 
for (auto it = myVec.begin(); it != myVec.end(); ++it) 
    (*it).func(); 

Tuy nhiên, trong phần còn lại 5% các trường hợp tôi muốn làm một cái gì đó khác nhau để các lớp tùy thuộc vào lớp con của họ . Và tôi có nghĩa là hoàn toàn khác nhau, giống như chức năng gọi điện thoại mà có các thông số khác hoặc không gọi chức năng ở tất cả cho một số lớp con. Tôi đã nghĩ đến một số tùy chọn để giải quyết vấn đề này, không có tùy chọn nào trong số đó tôi thực sự thích:

  • Sử dụng dynamic_cast để phân tích phân lớp. Không tốt, quá chậm khi tôi thực hiện cuộc gọi thường xuyên và trên phần cứng có giới hạn
  • Sử dụng cờ trong mỗi phân lớp, như một enum {IS_SUBCLASS_B, IS_SUBCLASS_C}. Không tốt vì nó không cảm thấy OO.
  • Cũng đặt các lớp trong các vectơ khác, mỗi lớp cho nhiệm vụ cụ thể của chúng. Điều này không cảm thấy thực sự OO, nhưng có lẽ tôi sai ở đây. Giống như:

    std::vector<std::shared_ptr<B> > vecForDoingSpecificOperation; 
    std::vector<std::shared_ptr<C> > vecForDoingAnotherSpecificOperation; 
    

Vì vậy, có thể một người nào đó đề nghị một phong cách/mô hình mà đạt được những gì mà tôi muốn?

+0

Nếu bạn đang truyền động cho một chế độ phụ bậc nhất cố định (ví dụ: không thực hiện chuỗi thử-động-phôi), thì hãy đi với nó, vì đó là cách rõ ràng nhất để đạt được điều này. Tốt nhất là thêm một giao diện và dàn diễn động vào giao diện. –

+0

Không rõ là bạn muốn có một giải pháp OO rõ ràng, hoặc nếu bạn muốn chạy mã của bạn và chăm sóc cho hiệu suất. Kể từ khi sao chép 'shared_ptr's là giá rẻ, chỉ cần công cụ nó trong' std :: vector 'khác nhau như bạn đề nghị. Bạn sẽ không có nhánh bên trong vòng lặp, đó có thể là điểm cộng cho hiệu suất. – eudoxos

Trả lời

30

Một người nào đó thông minh (tiếc là tôi đã quên ai) đã từng nói về OOP trong C++: Lý do duy nhất cho switch -nhiều loại (đó là những gì tất cả các đề xuất của bạn đề xuất) là sợ các chức năng ảo. (Đó là sự diễn giải). Thêm các chức năng ảo vào lớp cơ sở của bạn mà các lớp dẫn xuất có thể ghi đè và bạn đã được thiết lập.
Bây giờ, tôi biết có những trường hợp khó khăn hoặc khó sử dụng. Cho rằng chúng tôi có mẫu khách truy cập.

Có trường hợp tốt hơn và trường hợp khác. Thông thường, các nguyên tắc nhỏ đi như thế này:

  • Nếu bạn có một thay tập cố định các hoạt động, nhưng giữ loại thêm, sử dụng chức năng ảo.
    Các thao tác khó có thể thêm vào/gỡ bỏ từ một hệ thống phân cấp thừa kế lớn, nhưng các kiểu mới dễ dàng được thêm vào bằng cách đơn giản là có chúng ghi đè các hàm ảo thích hợp.

  • Nếu bạn có một thay tập cố định các loại, nhưng giữ hoạt động thêm, sử dụng visitor pattern.
    Việc thêm loại mới cho một nhóm khách truy cập lớn là một cơn đau nghiêm trọng ở cổ, nhưng việc thêm khách truy cập mới vào một nhóm loại cố định rất dễ dàng.

(Nếu cả hai thay đổi, bạn đang cam chịu một trong hai cách.)

+4

Và không đề cập đến mô hình khách truy cập nên được thực hiện mà không có một liên kết đến [Boost.Variant] (http://www.boost.org/libs/variant/). : -] – ildjarn

+1

Quy tắc chung tôi sử dụng là: khi đối mặt với mẫu khách truy cập, hãy xem xét sử dụng ** ngôn ngữ khác **. Thật vậy, bạn đang làm mô hình phù hợp, và các ngôn ngữ chức năng nổi trội ở đây. –

+0

Lời khuyên chung chung. Nhưng cho phép nói rằng tôi tiếp tục thêm các loại. Chức năng ảo sẽ không giúp tôi giải quyết lớp con theo cách tốt đẹp? – Frank

4

Theo bình luận của bạn, những gì bạn đã stumbled khi được biết đến (hồ nghi) là Expression Problem, được thể hiện bởi Philip Wadler:

Vấn đề biểu thức là tên mới cho một vấn đề cũ. Mục tiêu là định nghĩa một kiểu dữ liệu theo các trường hợp, nơi người ta có thể thêm các trường hợp mới vào kiểu dữ liệu và các hàm mới trên kiểu dữ liệu, mà không biên dịch lại mã hiện có, và trong khi vẫn giữ an toàn kiểu tĩnh (ví dụ, không có phôi).

Đó là, mở rộng cả "chiều dọc" (thêm các loại cho hệ thống phân cấp) và "ngang" (thêm chức năng để được overriden đến lớp cơ sở) là cứng trên các lập trình viên.

Có một cuộc thảo luận dài (luôn luôn) về nó trên Reddit mà tôi đã đề xuất một solution in C++.

Đây là cầu nối giữa OO (tuyệt vời khi thêm loại mới) và lập trình chung (tuyệt vời khi thêm các chức năng mới). Ý tưởng là có một loạt các giao diện thuần túy và một tập hợp các loại không đa hình. Các hàm tự do được định nghĩa trên các loại cụ thể khi cần và cầu nối với giao diện thuần túy được mang bởi một lớp mẫu đơn cho mỗi giao diện (được bổ sung bởi một hàm mẫu để khấu trừ tự động).

Tôi đã tìm thấy một hạn chế duy nhất cho đến nay: nếu một hàm trả về một giao diện Base, nó có thể đã được tạo ra, ngay cả khi loại thực tế được hỗ trợ nhiều hoạt động hơn. Đây là điển hình của một thiết kế mô-đun (các chức năng mới không có sẵn tại trang web cuộc gọi). Tôi nghĩ rằng nó minh họa một thiết kế sạch sẽ, tuy nhiên tôi hiểu rằng người ta có thể muốn "recast" nó vào một giao diện dài dòng hơn. Go có thể, với hỗ trợ ngôn ngữ (về cơ bản, thời gian chạy nội suy của các phương pháp có sẵn). Tôi không muốn mã số này bằng C++.


Như đã giải thích cho bản thân về reddit ... Tôi sẽ chỉ tái tạo và chỉnh sửa mã mà tôi đã gửi ở đó.

Vì vậy, hãy bắt đầu với 2 loại và một thao tác đơn lẻ.

struct Square { double side; }; 
double area(Square const s); 

struct Circle { double radius; }; 
double area(Circle const c); 

Bây giờ, chúng ta hãy làm một giao diện Shape:

class Shape { 
public: 
    virtual ~Shape(); 

    virtual double area() const = 0; 

protected: 
    Shape(Shape const&) {} 
    Shape& operator=(Shape const&) { return *this; } 
}; 

typedef std::unique_ptr<Shape> ShapePtr; 

template <typename T> 
class ShapeT: public Shape { 
public: 
    explicit ShapeT(T const t): _shape(t) {} 

    virtual double area() const { return area(_shape); } 

private: 
    T _shape; 
}; 

template <typename T> 
ShapePtr newShape(T t) { return ShapePtr(new ShapeT<T>(t)); } 

Okay, C++ là tiết. Hãy kiểm tra việc sử dụng ngay lập tức:

double totalArea(std::vector<ShapePtr> const& shapes) { 
    double total = 0.0; 
    for (ShapePtr const& s: shapes) { total += s->area(); } 
    return total; 
} 

int main() { 
    std::vector<ShapePtr> shapes{ new_shape<Square>({5.0}), new_shape<Circle>({3.0}) }; 

    std::cout << totalArea(shapes) << "\n"; 
} 

Vì vậy, tập thể dục đầu tiên, chúng ta hãy thêm một hình dạng (vâng, đó là tất cả):

struct Rectangle { double length, height; }; 
double area(Rectangle const r); 

Được rồi, cho đến nay tốt như vậy, chúng ta hãy thêm một hàm mới. Chúng tôi có hai lựa chọn.

Đầu tiên là sửa đổi Shape nếu nó nằm trong quyền lực của chúng tôi. Đây là nguồn tương thích, nhưng không tương thích nhị phân.

// 1. We need to extend Shape: 
    virtual double perimeter() const = 0 

// 2. And its adapter: ShapeT 
    virtual double perimeter() const { return perimeter(_shape); } 

// 3. And provide the method for each Shape (obviously) 
double perimeter(Square const s); 
double perimeter(Circle const c); 
double perimeter(Rectangle const r); 

Có vẻ như chúng tôi rơi vào vấn đề biểu thức ở đây, nhưng chúng tôi không. Chúng ta cần thêm chu vi cho mỗi lớp (đã biết) vì không có cách nào để tự động suy ra nó; tuy nhiên nó không yêu cầu chỉnh sửa từng lớp!

Do đó, sự kết hợp của Giao diện bên ngoài và các chức năng miễn phí cho phép chúng tôi gọn gàng (tốt, đó là C++ ...) bên lề vấn đề.

sodraz nhận thấy trong nhận xét rằng việc thêm chức năng đã chạm vào giao diện gốc có thể cần phải được cố định (do bên thứ ba cung cấp hoặc các vấn đề tương thích nhị phân).

Các tùy chọn thứ hai do đó không xâm nhập, với chi phí là hơi dài dòng hơn:

class ExtendedShape: public Shape { 
public: 
    virtual double perimeter() const = 0; 
protected: 
    ExtendedShape(ExtendedShape const&) {} 
    ExtendedShape& operator=(ExtendedShape const&) { return *this; } 
}; 

typedef std::unique_ptr<ExtendedShape> ExtendedShapePtr; 

template <typename T> 
class ExtendedShapeT: public ExtendedShape { 
public: 
    virtual double area() const { return area(_data); } 
    virtual double perimeter() const { return perimeter(_data); } 
private: 
    T _data; 
}; 

template <typename T> 
ExtendedShapePtr newExtendedShape(T t) { return ExtendedShapePtr(new ExtendedShapeT<T>(t)); } 

Và sau đó, xác định perimeter chức năng cho tất cả những Shape chúng tôi muốn sử dụng với ExtendedShape.

Mã cũ, được biên dịch để hoạt động với Shape, vẫn hoạt động. Nó không cần chức năng mới.

Mã mới có thể sử dụng chức năng mới và vẫn giao diện không đau đớn với mã cũ. (*)

Chỉ có một vấn đề nhỏ, nếu mã cũ trả về ShapePtr, chúng tôi không biết liệu hình dạng thực sự có chức năng chu vi hay không (lưu ý: nếu con trỏ được tạo bên trong, nó không được tạo bằng cơ chế newExtendedShape). Đây là giới hạn của thiết kế được đề cập ở đầu. Rất tiếc :)

(*) Lưu ý: không có nghĩa là bạn biết chủ sở hữu là ai. Một std::unique_ptr<Derived>&std::unique_ptr<Base>& không tương thích, tuy nhiên, std::unique_ptr<Base> có thể được tạo từ một số std::unique_ptr<Derived>Base* từ một Derived* để đảm bảo chức năng của bạn sạch sẽ và bạn vàng.

+0

"* Tôi không muốn viết mã trong C++. *": - [ – ildjarn

+0

@ildjarn: Ah! Tất nhiên người ta nên đọc * Tôi không muốn mã ** này ** trong C++ *, bởi vì thêm nội tâm mà không hỗ trợ ngôn ngữ là một nỗi đau trong ... –

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