2012-05-07 18 views
18

Tôi muốn kế thừa từ std::map, nhưng theo như tôi biết std::map không có bất kỳ destructor ảo nào.C++: Kế thừa từ std :: map

Do đó, có thể gọi điện đến số std::map của hàm hủy một cách rõ ràng trong trình phá hủy của tôi để đảm bảo hủy đối tượng thích hợp không?

Trả lời

28

Trình phá hủy không được gọi, ngay cả khi nó không phải là ảo, nhưng đó không phải là vấn đề.

Bạn nhận được hành vi không xác định nếu bạn cố xóa đối tượng thuộc loại của mình thông qua con trỏ đến std::map.

Sử dụng thành phần thay vì thừa kế, std vùng chứa không có nghĩa là được kế thừa và bạn không nên.

tôi giả sử bạn muốn mở rộng các chức năng của std::map (nói rằng bạn muốn tìm giá trị nhỏ nhất), trong trường hợp này bạn có hai tốt hơn, và pháp, lựa chọn:

1) Như gợi ý, bạn có thể sử dụng thành phần thay vì:

template<class K, class V> 
class MyMap 
{ 
    std::map<K,V> m; 
    //wrapper methods 
    V getMin(); 
}; 

2) chức năng miễn phí:

namespace MapFunctionality 
{ 
    template<class K, class V> 
    V getMin(const std::map<K,V> m); 
} 
+9

+1 Luôn ưu tiên bố cục thay vì thừa kế. Vẫn muốn có một số cách để giảm tất cả các mã boilerplate cần thiết cho gói. – daramarak

+0

@daramarak: do đó, tôi, nếu chỉ có một cái gì đó như 'using attribute.insert;' có thể hoạt động! Mặt khác, nó khá hiếm khi bạn thực sự cần tất cả các phương thức, và gói cho cơ hội để cung cấp tên có ý nghĩa và lấy các loại mức cao hơn :) –

+3

@daramarak: * Vẫn muốn có một số cách để giảm tất cả mã boilerplate cần thiết để gói *: có, đó là: thừa kế. Nhưng các lập trình viên tự thuyết phục họ không nên sử dụng nó ... bởi vì họ luôn có xu hướng giải thích nó là "là một". Nhưng đó không phải là một yêu cầu, chỉ là một sự thuyết phục công khai. –

13

Tôi muốn kế thừa từ std::map [...]

Tại sao?

Có hai lý do truyền thống để kế thừa:

  • để tái sử dụng giao diện của nó (và do đó, các phương pháp mã hóa chống lại nó)
  • để tái sử dụng hành vi của nó

Cựu vô nghĩa ở đây như map không có phương thức virtual nào để bạn không thể sửa đổi hành vi của nó bằng cách kế thừa; và sau này là một sự đảo lộn của việc sử dụng thừa kế mà chỉ phức tạp duy trì việc bảo trì cuối cùng.


Không có ý tưởng rõ ràng về mục đích sử dụng của bạn (thiếu bối cảnh trong câu hỏi), tôi cho rằng bạn muốn cung cấp một vùng chứa giống bản đồ. Có hai cách để đạt được điều này:

  • thành phần: bạn tạo một đối tượng mới, mà chứa một std::map, và cung cấp các giao diện đầy đủ
  • mở rộng: bạn tạo miễn phí chức năng mới hoạt động trên std::map

Cách sau đơn giản hơn, tuy nhiên nó cũng mở hơn: giao diện gốc của std::map vẫn mở rộng; do đó, không phù hợp cho hạn chế hoạt động.

Trước đây là nặng hơn, chắc chắn, nhưng cung cấp nhiều khả năng hơn.

Việc quyết định phương pháp nào phù hợp hơn là tùy thuộc vào bạn.

12

Có một quan niệm sai lầm: kế thừa-outide khái niệm về OOP tinh khiết, mà C++ không - là không có gì hơn một "thành phần với một thành viên không tên, với khả năng phân rã".

Sự vắng mặt của các hàm ảo (và hàm hủy không phải là đặc biệt, theo nghĩa này) làm cho đối tượng của bạn không đa hình, nhưng nếu những gì bạn đang làm chỉ là "tái sử dụng hành vi và phơi bày giao diện gốc" yêu cầu.

Cấu trúc không nhất thiết phải được gọi một cách rõ ràng, vì cuộc gọi của chúng luôn bị xích theo đặc điểm kỹ thuật.

#include <iostream> 
unsing namespace std; 

class A 
{ 
public: 
    A() { cout << "A::A()" << endl; } 
    ~A() { cout << "A::~A()" << endl; } 
    void hello() { cout << "A::hello()" << endl; } 
}; 

class B: public A 
{ 
public: 
    B() { cout << "B::B()" << endl; } 
    ~B() { cout << "B::~B()" << endl; } 
    void hello() { cout << "B::hello()" << endl; } 
}; 

int main() 
{ 
    B b; 
    b.hello(); 
    return 0; 
} 

chí đầu ra

A::A() 
B::B() 
B::hello() 
B::~B() 
A::~A() 

thực hiện một nhúng vào B với

class B 
{ 
public: 
    A a; 
    B() { cout << "B::B()" << endl; } 
    ~B() { cout << "B::~B()" << endl; } 
    void hello() { cout << "B::hello()" << endl; } 
}; 

rằng sẽ ra giống hệt nhau.

"Không lấy được nếu hàm hủy không phải là ảo" không phải là hậu quả bắt buộc C++, nhưng chỉ được chấp nhận không được viết (không có gì trong thông số về nó: ngoài quy tắc UB gọi xóa trên cơ sở) phát sinh trước C++ 99, khi OOP bởi thừa kế động và các hàm ảo là mô hình lập trình C++ duy nhất được hỗ trợ.

Tất nhiên, nhiều lập trình viên trên thế giới làm xương của chúng với loại trường đó (giống như dạy iostreams như nguyên thủy, sau đó chuyển sang mảng và con trỏ, và bài học cuối cùng giáo viên nói "oh. .. tehre cũng là STL có vectơ, chuỗi và các tính năng nâng cao khác ") và ngày nay, ngay cả khi C++ bị đa nguyên, vẫn nhấn mạnh với quy tắc OOP thuần túy này.

Trong mẫu của tôi A :: ~ A() không phải là ảo chính xác như A :: hello. Nó có nghĩa là gì?

Đơn giản: vì lý do tương tự, gọi A::hello sẽ không dẫn đến việc gọi B::hello, gọi số A::~A() (bằng cách xóa) sẽ không dẫn đến B::~B(). Nếu bạn có thể chấp nhận -rong phong cách lập trình của bạn- xác nhận đầu tiên, không có lý do nào bạn không thể chấp nhận số thứ hai. Trong mẫu của tôi, không có A* p = new B sẽ nhận được delete p vì A :: ~ A không phải là ảo và Tôi biết ý nghĩa của nó là.

Chính xác cùng một lý do sẽ không thực hiện, sử dụng ví dụ thứ hai cho B, A* p = &((new B)->a); với delete p;, mặc dù trường hợp thứ hai này hoàn toàn kép với trường hợp đầu tiên, trông không thú vị với bất kỳ ai.

Vấn đề duy nhất là "bảo trì", theo nghĩa là - mã yopur được xem bởi một lập trình viên OOP- sẽ từ chối nó, không phải vì nó sai, mà bởi vì anh ta đã được yêu cầu làm như vậy.Trong thực tế, "không lấy được nếu destructor không phải là ảo" là bởi vì hầu hết các lập trình viên beleave rằng có quá nhiều lập trình viên không biết họ không thể gọi xóa trên một con trỏ đến một cơ sở. (Xin lỗi nếu điều này là không lịch sự, nhưng sau hơn 30 năm kinh nghiệm lập trình tôi không thể nhìn thấy bất kỳ lý do nào khác!)

Nhưng câu hỏi của bạn là khác nhau:

Gọi B :: ~ B() (bằng cách xóa hoặc theo phạm vi kết thúc) sẽ luôn dẫn đến A :: ~ A() kể từ A (cho dù được nhúng hoặc thừa hưởng) là trong mọi trường hợp một phần của B.


Tiếp theo ý kiến ​​Luchian: Không xác định hành vi ám chỉ ở trên một trong ý kiến ​​của ông có liên quan đến một xóa trên một con trỏ-to-an-object's-base không có destructor ảo.

Theo trường OOP, kết quả này trong quy tắc "không xuất phát nếu không có destructor ảo tồn tại". Điều tôi chỉ ra, ở đây, là lý do của trường đó phụ thuộc vào thực tế là mọi đối tượng định hướng OOP phải đa hình và mọi thứ đều đa hình phải được định vị bằng con trỏ tới cơ sở, cho phép thay thế đối tượng . Bằng cách đưa ra những khẳng định đó, trường học cố tình làm mất khoảng cách giữa giao lộ và không thể thay thế, do đó chương trình OOP thuần túy sẽ không trải nghiệm UB đó.

Vị trí của tôi, đơn giản, thừa nhận rằng C++ không chỉ là OOP, và không phải tất cả các đối tượng C++ phải được định hướng theo mặc định, và thừa nhận OOP không phải lúc nào cũng là nhu cầu cần thiết. nhất thiết phải phục vụ để thay thế OOP.

std :: bản đồ không phải là đa hình nên không thể thay thế được. MyMap là giống nhau: KHÔNG đa hình và KHÔNG thể thay thế.

Nó chỉ đơn giản là phải tái sử dụng std :: bản đồ và vạch trần cùng một giao diện bản đồ ::. Và thừa kế chỉ là cách để tránh một bản mẫu dài của các hàm viết lại mà chỉ cần gọi những cái được sử dụng lại.

MyMap sẽ không có dtor ảo như std :: bản đồ không có. Và điều này - với tôi- là đủ để nói với một lập trình viên C++ rằng đây không phải là các đối tượng đa hình và không được sử dụng ở nơi khác.

Tôi phải thừa nhận vị trí này hiện không được chia sẻ bởi hầu hết các chuyên gia C++. Nhưng tôi nghĩ (ý kiến ​​cá nhân duy nhất của tôi) đây chỉ là vì lịch sử của họ, liên quan đến OOP như một giáo điều để phục vụ, không phải vì nhu cầu C++. Với tôi C++ không phải là một ngôn ngữ OOP thuần túy và không nhất thiết phải luôn tuân theo mô hình OOP, trong một ngữ cảnh mà OOP không được theo dõi hoặc yêu cầu.

+3

Bạn đang thực hiện một số câu lệnh nguy hiểm ở đó. Đừng coi sự cần thiết cho một destructor ảo là lỗi thời. Tiêu chuẩn ** nêu rõ ** rằng hành vi không xác định phát sinh trong tình huống tôi đã đề cập. Trừu tượng là một phần quan trọng của OOP. Điều đó có nghĩa là bạn không chỉ bắt nguồn từ để tái sử dụng, mà còn để ẩn các loại thực tế. Có nghĩa là, trong một thiết kế tốt, nếu bạn sử dụng thừa kế, bạn sẽ kết thúc với 'std :: map *' thực sự trỏ đến 'MyMap'. Và nếu bạn xóa nó, bất cứ điều gì có thể xảy ra, bao gồm cả một vụ tai nạn. –

+4

@LuchianGrigore: * Tiêu chuẩn nêu rõ rằng hành vi không xác định phát sinh trong tình huống tôi đã đề cập. *. Đúng, nhưng đây không phải là tình huống mà tôi đã đề cập, và không phải là tình huống mà OP đã đề cập. * Có nghĩa là, trong một thiết kế tốt, nếu bạn sử dụng thừa kế, bạn sẽ kết thúc với std :: map * thực sự trỏ đến MyMap *: đó là nói chung FALSE, và chỉ đúng với con trỏ thuần túy dựa trên OOP. Đó là exactuy những gì mẫu của tôi là KHÔNG. Làm thế nào để bạn giải thích sự tồn tại của các mẫu của tôi, mà không sử dụng đa hình và con trỏ ở tất cả? –

+2

@LuchianGrigore: Dù sao, tôi nghĩ rằng bạn là * chính xác *: những gì tôi khẳng định là nguy hiểm, nhưng không phải cho chương trình đúng đắn, nhưng đối với nền văn hóa dựa trên chương trình OOP! Nhưng đừng lo lắng: phản ứng của bạn đã được mong đợi! –