Tại sao không có lớp cơ sở trong C ++?


84

Câu hỏi nhanh: từ quan điểm thiết kế, tại sao trong C ++, không có lớp cơ sở mẹ của tất cả, những gì thường có objecttrong các ngôn ngữ khác?


27
Đây thực sự là một câu hỏi triết học hơn là một câu hỏi thiết kế. Câu trả lời ngắn gọn là "bởi vì Bjarne rõ ràng không muốn làm điều đó."
Jerry Coffin

4
Cũng không có gì ngăn cản bạn làm một cái.
GWW

22
Cá nhân tôi nghĩ rằng đó là một lỗ hổng thiết kế khi có mọi thứ bắt nguồn từ cùng một lớp cơ sở. Nó thúc đẩy một phong cách lập trình gây ra nhiều vấn đề hơn mức đáng có (vì bạn có xu hướng có các vùng chứa đối tượng chung mà sau đó bạn cần phân biệt để sử dụng đối tượng thực tế (Điều này tôi cau mày vì thiết kế xấu IMHO)). Tôi có thực sự muốn một thùng chứa có thể chứa cả ô tô và điều khiển các đối tượng tự động hóa không?
Martin York

3
@Martin nhưng hãy nhìn nó theo cách này: ví dụ, cho đến khi C ++ 0x auto, bạn phải sử dụng các định nghĩa kiểu dài hàng dặm cho các trình vòng lặp, hoặc một lần typedef. Với hệ thống phân cấp lớp chung chung hơn, bạn chỉ có thể sử dụng objecthoặc iterator.
slzica

9
@Santiago: Việc sử dụng một hệ thống loại thống nhất hầu như luôn có nghĩa là bạn phải kết thúc với rất nhiều mã dựa vào tính đa hình, điều phối động và RTTI, tất cả đều tương đối đắt tiền và tất cả đều hạn chế tối ưu hóa có thể thực hiện được khi chúng không được sử dụng.
James McNellis

Câu trả lời:


116

Phán quyết cuối cùng được tìm thấy trong Câu hỏi thường gặp của Stroustrup . Tóm lại, nó không truyền tải bất kỳ ý nghĩa ngữ nghĩa nào. Nó sẽ có một chi phí. Các mẫu hữu ích hơn cho các thùng chứa.

Tại sao C ++ không có Đối tượng lớp phổ quát?

  • Chúng tôi không cần một: lập trình chung cung cấp các lựa chọn thay thế an toàn kiểu tĩnh trong hầu hết các trường hợp. Các trường hợp khác được xử lý bằng cách sử dụng đa kế thừa.

  • Không có lớp phổ quát hữu ích: lớp phổ quát thực sự không mang ngữ nghĩa của riêng nó.

  • Một lớp "phổ thông" khuyến khích suy nghĩ cẩu thả về các loại và giao diện và dẫn đến việc kiểm tra thời gian chạy quá mức.

  • Sử dụng một lớp cơ sở phổ quát ngụ ý chi phí: Các đối tượng phải được phân bổ theo đống để có thể đa hình; điều đó có nghĩa là bộ nhớ và chi phí truy cập. Các đối tượng Heap không hỗ trợ ngữ nghĩa sao chép một cách tự nhiên. Các đối tượng Heap không hỗ trợ hành vi theo phạm vi đơn giản (điều này làm phức tạp việc quản lý tài nguyên). Một lớp cơ sở phổ quát khuyến khích sử dụng dynamic_cast và kiểm tra thời gian chạy khác.


83
Chúng ta không cần không / đối tượng lớp cơ sở / chúng ta không cần không / nghĩ kiểm soát ...;)
Piskvor rời xây dựng

3
@Piskvor - LOL - Liên minh huyền thoại Tôi muốn xem video âm nhạc!
Byron Whitlock

10
Objects must be heap-allocated to be polymorphic- Tôi không nghĩ câu nói này nói chung là đúng. Bạn chắc chắn có thể tạo một thể hiện của lớp đa hình trên ngăn xếp và chuyển nó dưới dạng con trỏ đến một trong các lớp cơ sở của nó, mang lại hành vi đa hình.
Byron

5
Trong Java, cố gắng truyền một tham chiếu đến- Foothành một tham chiếu đến- Barkhi Barnào không kế thừa Foo, về mặt xác định sẽ ném ra một ngoại lệ. Trong C ++, việc cố gắng biến một con trỏ tới Foo thành một con trỏ tới Thanh có thể gửi một robot quay ngược thời gian để giết Sarah Connor.
supercat

3
@supercat Đó là lý do tại sao chúng tôi sử dụng dynamic_cast <>. Để tránh SkyNet và những chiếc ghế sofa Chesterfield xuất hiện một cách bí ẩn.
Captain Giraffe

44

Trước tiên, hãy nghĩ về lý do tại sao bạn muốn có một lớp cơ sở ngay từ đầu. Tôi có thể nghĩ ra một số lý do khác nhau:

  1. Để hỗ trợ các hoạt động hoặc tập hợp chung sẽ hoạt động trên các đối tượng thuộc bất kỳ loại nào.
  2. Bao gồm các thủ tục khác nhau phổ biến cho tất cả các đối tượng (chẳng hạn như quản lý bộ nhớ).
  3. Mọi thứ đều là một đối tượng (không có nguyên thủy!). Một số ngôn ngữ (như Objective-C) không có điều này, điều này làm cho mọi thứ trở nên khá lộn xộn.

Đây là hai lý do chính đáng mà các ngôn ngữ của thương hiệu Smalltalk, Ruby và Objective-C có các lớp cơ sở (về mặt kỹ thuật, Objective-C không thực sự có lớp cơ sở, nhưng đối với mọi ý định và mục đích, nó có).

Đối với # 1, nhu cầu về một lớp cơ sở hợp nhất tất cả các đối tượng trong một giao diện duy nhất đã bị loại bỏ bởi việc đưa các mẫu vào C ++. Ví dụ:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

là không cần thiết, khi bạn có thể duy trì tính toàn vẹn của kiểu thông qua phương tiện đa hình tham số!

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Đối với # 2, trong khi trong Objective-C, các thủ tục quản lý bộ nhớ là một phần của việc triển khai một lớp và được kế thừa từ lớp cơ sở, quản lý bộ nhớ trong C ++ được thực hiện bằng cách sử dụng thành phần thay vì kế thừa. Ví dụ: bạn có thể xác định một trình bao bọc con trỏ thông minh sẽ thực hiện đếm tham chiếu trên các đối tượng thuộc bất kỳ loại nào:

template <class T>
struct refcounted
{
  refcounted(T* object) : _object(object), _count(0) {}

  T* operator->() { return _object; }
  operator T*() { return _object; }

  void retain() { ++_count; }

  void release()
  {
    if (--_count == 0) { delete _object; }
  }

  private:
    T* _object;
    int _count;
};

Sau đó, thay vì gọi các phương thức trên chính đối tượng, bạn sẽ gọi các phương thức trong trình bao bọc của nó. Điều này không chỉ cho phép lập trình chung chung hơn: nó còn cho phép bạn tách biệt các mối quan tâm (vì lý tưởng, đối tượng của bạn nên quan tâm nhiều hơn đến những gì nó phải làm, hơn là cách quản lý bộ nhớ của nó trong các tình huống khác nhau).

Cuối cùng, trong một ngôn ngữ có cả đối tượng gốc và đối tượng thực tế như C ++, lợi ích của việc có một lớp cơ sở (một giao diện nhất quán cho mọi giá trị) sẽ bị mất, do đó bạn có một số giá trị không thể phù hợp với giao diện đó. Để sử dụng nguyên thủy trong loại tình huống đó, bạn cần nâng chúng thành các đối tượng (nếu trình biên dịch của bạn không tự động làm điều đó). Điều này tạo ra rất nhiều phức tạp.

Vì vậy, câu trả lời ngắn gọn cho câu hỏi của bạn: C ++ không có lớp cơ sở bởi vì, có đa hình tham số thông qua các mẫu, nó không cần thiết.


Được rồi! Rất vui vì bạn thấy nó hữu ích :)
Jonathan Sterling

2
Đối với điểm 3, tôi phản đối bằng C #. C # có các nguyên hàm có thể được đóng hộp thành object( System.Object), nhưng chúng không cần phải như vậy. Đối với trình biên dịch, intSystem.Int32là bí danh và có thể được sử dụng thay thế cho nhau; thời gian chạy xử lý quyền anh khi cần thiết.
Cole Johnson

Trong C ++, không cần quyền anh. Với các mẫu, trình biên dịch tạo ra mã chính xác tại thời điểm biên dịch.
Rob K

2
Xin lưu ý rằng trong khi câu trả lời (cũ) này thể hiện con trỏ thông minh được tính tham chiếu, C ++ 11 hiện có con trỏ này std::shared_ptrnên được sử dụng thay thế.

15

Mô hình chi phối cho các biến C ++ là truyền qua giá trị, không phải chuyển theo tham chiếu. Việc buộc mọi thứ phải bắt nguồn từ gốc Objectsẽ khiến việc chuyển chúng theo giá trị thành một lỗi ipse facto.

(Bởi vì việc chấp nhận một Đối tượng theo giá trị làm tham số, theo định nghĩa sẽ cắt nó và loại bỏ linh hồn của nó).

Đây là điều không được hoan nghênh. C ++ khiến bạn suy nghĩ về việc bạn muốn giá trị hay ngữ nghĩa tham chiếu, cho bạn sự lựa chọn. Đây là một điều quan trọng trong tính toán hiệu suất.


1
Nó sẽ không phải là một lỗi của riêng nó. Các cấu trúc phân cấp kiểu đã tồn tại và chúng sử dụng giá trị vượt qua khá tốt khi thích hợp. Tuy nhiên, nó sẽ khuyến khích nhiều chiến lược dựa trên heap hơn trong đó tham chiếu chuyển qua là thích hợp hơn.
Dennis Zickefoose

IMHO, một ngôn ngữ tốt nên có các kiểu kế thừa riêng biệt cho các trường hợp mà một lớp dẫn xuất có thể được sử dụng hợp pháp như một đối tượng lớp cơ sở bằng cách sử dụng từng tham chiếu, những đối tượng mà nó có thể được biến thành một bằng cách sử dụng từng giá trị, và những nơi không hợp pháp. Giá trị của việc có một loại đế chung cho những thứ có thể cắt lát sẽ bị hạn chế, nhưng nhiều thứ không thể cắt lát và phải được cất giữ gián tiếp; chi phí bổ sung để có những thứ như vậy bắt nguồn từ một điểm chung Objectsẽ tương đối nhỏ.
supercat

6

Vấn đề là có một kiểu như vậy trong C ++! Đó là void. :-) Bất kỳ con trỏ nào cũng có thể được truyền ngầm đến một cách an toàn void *, bao gồm các con trỏ đến các kiểu cơ bản, các lớp không có bảng ảo và các lớp có bảng ảo.

Vì nó phải tương thích với tất cả các loại đối tượng đó, nên voidbản thân nó không thể chứa các phương thức ảo. Nếu không có các hàm ảo và RTTI, không có thông tin hữu ích nào về loại có thể được lấy từ void(nó khớp với MỌI loại, vì vậy chỉ có thể cho biết những thứ đúng với MỌI loại), nhưng các hàm ảo và RTTI sẽ làm cho các loại đơn giản trở nên rất kém hiệu quả và ngăn cản C ++ một ngôn ngữ thích hợp cho lập trình cấp thấp với khả năng truy cập bộ nhớ trực tiếp, v.v.

Vì vậy, có loại như vậy. Nó chỉ cung cấp giao diện rất tối giản (thực tế là trống) do tính chất cấp thấp của ngôn ngữ. :-)


Các kiểu con trỏ và kiểu đối tượng không giống nhau. Bạn không thể có một đối tượng kiểu void.
Kevin Panko

1
Trên thực tế, đó là cách kế thừa trong C ++ thường hoạt động. Một trong những điều quan trọng nhất mà nó làm là khả năng tương thích của các kiểu con trỏ và tham chiếu: khi con trỏ đến một lớp được yêu cầu, một con trỏ đến lớp dẫn xuất có thể được cung cấp. Và khả năng con trỏ static_cast giữa hai loại là một dấu hiệu chúng được kết nối trong một hệ thống phân cấp. Từ quan điểm này, void chắc chắn là một lớp cơ sở phổ biến trong C ++.
Ellioh

Nếu bạn khai báo một biến kiểu, voidnó sẽ không biên dịch và bạn gặp lỗi này . Trong Java, bạn có thể có một biến kiểu Objectvà nó sẽ hoạt động. Đó là sự khác biệt giữa voidvà một loại "thực". Có lẽ đúng voidlà kiểu cơ sở cho mọi thứ, nhưng vì nó không cung cấp bất kỳ hàm tạo, phương thức hoặc trường nào nên không có cách nào để biết nó có ở đó hay không. Khẳng định này không thể được chứng minh hoặc bác bỏ.
Kevin Panko

1
Trong Java mọi biến là một tham chiếu. Đó là sự khác biệt. :-) (BTW, trong C ++ cũng có các lớp trừu tượng mà bạn không thể sử dụng để khai báo một biến). Và trong câu trả lời của mình, tôi đã giải thích lý do tại sao không thể lấy RTTI từ trạng thái vô hiệu. Vì vậy, về mặt lý thuyết void vẫn là một lớp cơ sở cho mọi thứ. static_cast là một bằng chứng, vì nó chỉ thực hiện ép kiểu giữa các kiểu có liên quan và nó có thể được sử dụng cho void. Nhưng bạn nói đúng, việc không có quyền truy cập RTTI thực sự làm cho rất khác với các loại cơ sở trong các ngôn ngữ cấp cao hơn.
Ellioh

1
@Ellioh Sau khi tham khảo tiêu chuẩn C ++ 11 §3.9.1p9, tôi thừa nhận rằng đó voidlà một kiểu (và phát hiện ra một cách sử dụng? : hơi thông minh ), nhưng nó vẫn còn lâu mới trở thành một lớp cơ sở phổ quát. Đối với một điều, nó là "một loại không hoàn chỉnh không thể hoàn thành," và cho một loại khác, không có void&loại nào .
bcrist

-2

C ++ là một ngôn ngữ được đánh máy mạnh. Tuy nhiên, thật khó hiểu là nó không có kiểu đối tượng phổ quát trong bối cảnh chuyên biệt hóa khuôn mẫu.

Lấy ví dụ, mẫu

template <class T> class Hook;
template <class ReturnType, class ... ArgTypes>
class Hook<ReturnType (ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

có thể được khởi tạo như

Hook<decltype(some_function)> ...;

Bây giờ, hãy giả sử rằng chúng ta muốn điều tương tự cho một hàm cụ thể. Giống

template <auto fallback> class Hook;
template <auto fallback, class ReturnType, class ... ArgTypes>
class Hook<ReturnType fallback(ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

với sự khởi tạo chuyên biệt

Hook<some_function> ...

Nhưng than ôi, mặc dù lớp T có thể thay thế cho bất kỳ kiểu nào (lớp hoặc không) trước chuyên môn hóa, không có từ nào tương đương auto fallback(tôi đang sử dụng cú pháp đó như cú pháp không phải kiểu chung chung rõ ràng nhất trong ngữ cảnh này) có thể thay thế cho bất kỳ đối số mẫu không phải kiểu trước khi chuyên môn hóa.

Vì vậy, nói chung mẫu này không chuyển từ đối số mẫu kiểu sang đối số mẫu không phải kiểu.

Giống như rất nhiều góc trong ngôn ngữ C ++, câu trả lời có thể là "không có thành viên ủy ban nào nghĩ ra".


Ngay cả khi (một cái gì đó như) của bạn ReturnType fallback(ArgTypes...)sẽ hoạt động, nó sẽ là một thiết kế tồi. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...làm những gì bạn muốn và có nghĩa là các thông số mẫu của Hooklà của phù hợp các loại
Caleth

-4

C ++ ban đầu được gọi là "C với các lớp". Nó là một bước tiến của ngôn ngữ C, không giống như một số thứ hiện đại hơn khác như C #. Và bạn không thể xem C ++ là một ngôn ngữ, mà là nền tảng của ngôn ngữ (Vâng, tôi đang nhớ cuốn sách của Scott Meyers Hiệu quả C ++).

Bản thân C là sự kết hợp của nhiều ngôn ngữ, ngôn ngữ lập trình C và bộ tiền xử lý của nó.

C ++ thêm một hỗn hợp khác:

  • phương pháp tiếp cận lớp / đối tượng

  • mẫu

  • STL

Cá nhân tôi không thích một số thứ đến trực tiếp từ C sang C ++. Một ví dụ là tính năng enum. Cách C # cho phép nhà phát triển sử dụng nó tốt hơn: nó giới hạn enum trong phạm vi riêng của nó, nó có thuộc tính Count và có thể dễ dàng lặp lại.

Vì C ++ muốn tương thích ngược với C, nhà thiết kế đã rất dễ dãi cho phép ngôn ngữ C nhập toàn bộ vào C ++ (có một số khác biệt nhỏ, nhưng tôi không nhớ bất kỳ điều gì bạn có thể làm bằng trình biên dịch C mà bạn không thể làm bằng trình biên dịch C ++).


"không thể coi C ++ là một ngôn ngữ, mà là nền tảng của các ngôn ngữ" ... quan tâm đến việc thực sự giải thích và minh họa câu nói kỳ lạ đó đang hướng tới điều gì? Nhiều mô hình khác biệt với "nhiều ngôn ngữ", mặc dù công bằng mà nói bộ tiền xử lý không tách rời với phần còn lại của các bước biên dịch - điểm tốt. Re enums - chúng có thể được bao bọc trong một phạm vi lớp nếu muốn và toàn bộ ngôn ngữ thiếu nội tâm - một vấn đề lớn, mặc dù trong trường hợp này, nó có thể gần đúng - hãy xem mã benum của Boost vault. Có phải quan điểm của bạn chỉ là Q: C ++ không bắt đầu với một cơ sở phổ quát?
Tony Delroy

@Tony: Thật kỳ lạ, cuốn sách tôi đã tham khảo điều duy nhất mà họ thậm chí không đề cập đến vì một ngôn ngữ khác là bộ tiền xử lý, đó là bộ duy nhất mà bạn xem xét riêng. Tôi hiểu quan điểm của bạn, vì trước tiên bộ xử lý phân tích cú pháp đầu vào của chính nó. Tuy nhiên, có một cơ chế tạo khuôn mẫu giống như có một ngôn ngữ khác thực hiện việc khái quát hóa cho bạn. Bạn áp dụng cú pháp mẫu và bạn sẽ có các chức năng tạo trình biên dịch cho mọi kiểu đơn lẻ mà bạn có. Khi bạn có thể có một chương trình viết bằng C và đưa ra ý kiến của mình cho một ++ biên dịch C, đây là hai ngôn ngữ trong một: C và C với các đối tượng => C ++
sergiol

STL, nó có thể được xem như một tiện ích bổ sung, nhưng nó có các lớp và vùng chứa rất thực tế mà thông qua các mẫu TỰ NHIÊN mở rộng sức mạnh của C ++. Và các enums mà tôi đang nói là enums NATIVE. Khi bạn nói về Boost, bạn đang dựa vào mã của bên thứ ba không phải là ngôn ngữ TỰ NHIÊN. Trong C #, tôi không cần phải bao gồm bất cứ thứ gì bên ngoài vì có một enum có thể lặp lại và tự xác định phạm vi. Trong C ++, một thủ thuật tôi thường sử dụng, là gọi mục cuối cùng của một enum - không có giá trị được gán, vì vậy chúng được tự động bắt đầu từ 0 và tăng dần - giống như NUM_ITEMS, và điều này cho tôi giới hạn trên.
sergiol

2
cảm ơn bạn đã giải thích thêm - ít nhất tôi có thể biết bạn đến từ đâu; Tôi đã nghe một vài người phàn nàn rằng việc học sử dụng các mẫu cảm thấy không giống với các lập trình C ++ khác mặc dù đó chắc chắn không phải là kinh nghiệm của tôi. Tôi sẽ để những độc giả khác làm điều họ muốn về những quan điểm khác mà bạn trình bày. Chúc mừng.
Tony Delroy

1
Tôi nghĩ vấn đề lớn nhất với việc có một kiểu cơ sở phổ quát là kiểu như vậy sẽ vô dụng nếu nó không có bất kỳ phương thức ảo nào; C ++ không muốn thêm bất kỳ chi phí nào cho các loại cấu trúc không có bất kỳ phương thức ảo nào, nhưng mọi đối tượng thuộc bất kỳ loại nào có bất kỳ phương thức ảo nào đều phải có, tối thiểu, con trỏ đến một bảng điều phối. Nếu các lớp và cấu trúc là nơi sinh sống của các vũ trụ khác nhau, thì có thể có một đối tượng lớp phổ quát, nhưng các lớp và cấu trúc về cơ bản giống nhau trong C ++.
supercat
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.