เหตุใดจึงเป็นพื้นฐานสำหรับวัตถุทั้งหมดท้อแท้ใน C ++


76

Stroustrup กล่าวว่า "อย่าคิดค้นฐานเฉพาะสำหรับคลาสทั้งหมดของคุณ (คลาส Object) โดยทันทีคุณสามารถทำได้ดีกว่านี้หากไม่มีคลาสที่มากที่สุด / ส่วนใหญ่" (ภาษา C ++ ภาษาที่สี่รุ่น Sec 1.3.4)

ทำไมพื้นฐานสำหรับทุกสิ่งโดยทั่วไปเป็นความคิดที่ไม่ดีและเมื่อใดที่เหมาะสมที่จะสร้าง


16
เนื่องจาก C ++ ไม่ใช่ Java ... และคุณไม่ควรพยายามบังคับให้เป็น
AK_

10
ถามเกี่ยวกับ Stack Overflow: ทำไมไม่มีคลาสพื้นฐานใน C ++

26
นอกจากนี้ฉันไม่เห็นด้วยกับคะแนนโหวตที่ใกล้ชิดสำหรับ มีเหตุผลที่เฉพาะเจาะจงมากที่สามารถอธิบายได้สำหรับเรื่องนี้เนื่องจากคำตอบยืนยันทั้งในคำถามนี้และคำถาม SO ที่เชื่อมโยง

2
มันคือ "คุณไม่ต้องการมัน" หลักการแห่งความคล่องตัว หากคุณไม่ได้ระบุความต้องการเฉพาะไว้ก่อนแล้วอย่าทำ (จนกว่าคุณจะทำ)
Jool

3
@AK_: มี "โง่เหมือน" หายไปในความคิดเห็นของคุณ
DeadMG

คำตอบ:


75

เพราะวัตถุนั้นจะมีฟังก์ชั่นการทำงานอย่างไร ในจาวาทุกคลาสฐานมีเป็น toString, hashCode & equality และตัวแปร monitor + condition

  • ToString มีประโยชน์สำหรับการดีบักเท่านั้น

  • hashCode จะเป็นประโยชน์เฉพาะถ้าคุณต้องการที่จะเก็บไว้ในคอลเลกชันแฮช-based (การตั้งค่าใน C ++ คือการผ่านฟังก์ชั่นคร่ำเครียดให้ภาชนะที่เป็นแม่แบบพารามิเตอร์หรือเพื่อหลีกเลี่ยงstd::unordered_*โดยสิ้นเชิงและแทนที่จะใช้std::vectorและรายชื่อเรียงลำดับธรรมดา)

  • ความเท่าเทียมกันโดยไม่ต้องวัตถุฐานสามารถช่วยในการรวบรวมเวลาหากพวกเขาไม่มีประเภทเดียวกันแล้วพวกเขาจะไม่เท่ากัน ใน C ++ นี่คือข้อผิดพลาดเวลาในการรวบรวม

  • ตัวแปรมอนิเตอร์และเงื่อนไขนั้นรวมไว้ดีกว่าอย่างชัดเจนในแต่ละกรณี

อย่างไรก็ตามเมื่อมีมากกว่าที่จำเป็นต้องทำแล้วมีกรณีใช้

ตัวอย่างเช่นใน QT มีQObjectคลาสรูทซึ่งเป็นพื้นฐานของความสัมพันธ์ของเธรดลำดับชั้นความเป็นเจ้าของแม่ลูกและกลไกช่องสัญญาณ นอกจากนี้ยังบังคับให้ใช้งานโดยตัวชี้สำหรับ QObjects อย่างไรก็ตามคลาสจำนวนมากใน Qt ไม่สืบทอด QObjectเนื่องจากไม่จำเป็นต้องใช้ช่องสัญญาณ (โดยเฉพาะประเภทค่าของคำอธิบายบางอย่าง)


7
คุณลืมที่จะพูดถึงอาจเป็นเหตุผลหลักที่ Java มีคลาสพื้นฐาน: ก่อนที่จะเรียนทั่วไป, คอลเลกชันคลาสจำเป็นต้องใช้คลาสพื้นฐานในการทำงาน ทุกอย่าง (จัดเก็บข้อมูลภายในพารามิเตอร์ค่ากลับ) Objectถูกพิมพ์
Aleksandr Dubinsky

1
@AleksandrDubinsky: และยาชื่อสามัญเพียงเพิ่มน้ำตาล syntactic ไม่ได้เปลี่ยนจริงๆ แต่สิ่งที่ขัด
Deduplicator

4
ฉันจะโต้แย้งรหัสแฮชความเท่าเทียมกันและการสนับสนุนการตรวจสอบเป็นความผิดพลาดในการออกแบบใน Java เช่นกัน ใครคิดว่าเป็นความคิดที่ดีที่จะทำให้วัตถุทั้งหมดเป็นล็อค!
usr

1
ใช่ แต่ไม่มีใครต้องการมัน เมื่อครั้งสุดท้ายที่คุณจำเป็นต้องล็อควัตถุและไม่สามารถสร้างอินสแตนซ์ของวัตถุล็อคที่แยกต่างหากเพื่อทำเช่นนั้นได้ มันหายากมากและทำให้ทุกอย่างเป็นภาระ พวก Java มีความเข้าใจที่ไม่ดีเกี่ยวกับความปลอดภัยของเธรดตั้งแต่นั้นมาเป็นหลักฐานจากวัตถุทั้งหมดที่ถูกล็อคและคอลเลกชันที่ปลอดภัยของเธรดที่เลิกใช้แล้ว ความปลอดภัยของเธรดเป็นทรัพย์สินส่วนกลางไม่ใช่แบบต่อวัตถุ
usr

2
" hashCode จะมีประโยชน์ก็ต่อเมื่อคุณต้องการเก็บไว้ในคอลเลกชันที่ใช้แฮช (การตั้งค่าใน C ++ สำหรับ std :: vector และรายการที่ไม่มีการเรียงลำดับธรรมดา) " counterargument จริง ๆ_hashCodeไม่ใช่ 'ใช้คอนเทนเนอร์อื่น' แต่เป็นการชี้ out ที่ C ++ std::unordered_mapทำ hashing โดยใช้อาร์กิวเมนต์แม่แบบแทนที่จะต้องระดับองค์ประกอบตัวเองเพื่อให้การใช้งาน นั่นคือเช่นเดียวกับคอนเทนเนอร์และตัวจัดการทรัพยากรที่ดีอื่น ๆ ทั้งหมดใน C ++ มันไม่ได้เป็นการล่วงล้ำ ไม่ก่อให้เกิดมลพิษกับวัตถุทั้งหมดที่มีฟังก์ชั่นหรือข้อมูลในกรณีที่บางคนอาจต้องการวัตถุเหล่านั้นในบางบริบทในภายหลัง
underscore_d

100

เพราะไม่มีฟังก์ชั่นที่ใช้ร่วมกันโดยวัตถุทั้งหมด ไม่มีอะไรให้ใส่ในส่วนต่อประสานนี้ที่เหมาะสมสำหรับทุกชั้นเรียน


10
+1 เพื่อความเรียบง่ายของคำตอบนี่เป็นเหตุผลเดียวที่ทำให้
BWG

7
ในเฟรมเวิร์กขนาดใหญ่ที่ฉันมีประสบการณ์คลาสพื้นฐานทั่วไปจัดเตรียมโครงสร้างการทำให้เป็นอนุกรมและโครงสร้างการสะท้อนที่ต้องการในบริบท <สิ่งใด> Meh มันแค่ส่งผลให้คนที่ซีเรียลของ cruft พร้อมกับข้อมูลและเมตาดาต้าและทำให้รูปแบบข้อมูลมีขนาดใหญ่เกินไปและซับซ้อนเกินกว่าจะมีประสิทธิภาพ
dmckee

19
@dmckee: ฉันยังยืนยันว่าการทำให้เป็นอันดับและการสะท้อนกลับเป็นความต้องการที่มีประโยชน์ในระดับสากล
DeadMG

16
@DeadMG: "แต่ถ้าคุณต้องการประหยัดทุกอย่าง"
deworde

8
ฉันไม่รู้คุณติดมันด้วยเครื่องหมายคำพูดคุณใช้ตัวพิมพ์ใหญ่ทั้งหมดและผู้คนไม่สามารถเห็นเรื่องตลกได้ @MSalters: นั่นมันง่ายนะมันมีสถานะน้อยที่สุดคุณแค่ระบุว่ามันอยู่ตรงนั้น ฉันสามารถเขียนชื่อของฉันลงในรายการโดยไม่ต้องวนซ้ำซ้ำ
deworde

25

เมื่อใดก็ตามที่คุณสร้างลำดับชั้นมรดกสูงของวัตถุที่คุณมีแนวโน้มที่จะทำงานเป็นปัญหาของฐานชั้นเปราะบาง (วิกิพีเดีย.)

การมีลำดับชั้นการสืบทอดขนาดเล็ก (แตกต่างกันอย่างชัดเจนและแยก) ขนาดเล็กจำนวนมากช่วยลดโอกาสในการเกิดปัญหานี้

การทำให้วัตถุทั้งหมดของคุณเป็นส่วนหนึ่งของลำดับชั้นการสืบทอดที่มีขนาดมหึมาเพียงอันเดียวรับประกันได้ว่าคุณกำลังประสบกับปัญหานี้


6
เมื่อคลาสพื้นฐาน (ใน Java "java.lang.Object") ไม่มีวิธีการใด ๆ ที่เรียกเมธอดอื่นปัญหา Fragile Base Class จะไม่เกิดขึ้น
Martin Rosenau

3
คลาสฐานที่มีประโยชน์อันยิ่งใหญ่ที่จะเป็น!
Mike Nakis

9
@MartinRosenau ... เหมือนที่คุณสามารถทำได้ใน C ++ โดยไม่จำเป็นต้องมีคลาสฐานหลัก!
gbjbaanb

5
@ DavorŽdraloดังนั้น C ++ มีชื่อโง่สำหรับฟังก์ชั่นพื้นฐาน ("โอเปอเรเตอร์ <<" แทนที่จะเป็นสิ่งที่สมเหตุสมผลเช่น "DebugPrint") ในขณะที่ Java มีคลาสพื้นฐานสำหรับทุกคลาสที่คุณเขียนอย่างไม่มีข้อยกเว้น ฉันคิดว่าฉันชอบหูดมากขึ้นของ C ++
เซบาสเตียนเรดล

4
@ DavorŽdralo: ชื่อของฟังก์ชั่นไม่เกี่ยวข้อง ภาพไวยากรณ์cout.print(x).print(0.5).print("Bye\n")- operator<<มันไม่ได้ขึ้นอยู่กับ
MSalters

24

เพราะ:

  1. คุณไม่ควรจ่ายเงินสำหรับสิ่งที่คุณไม่ได้ใช้
  2. ฟังก์ชั่นเหล่านี้มีความหมายน้อยกว่าในระบบประเภทที่อิงตามมูลค่ามากกว่าในระบบที่อ้างอิงตามประเภท

การใช้ฟังก์ชั่นประเภทใดก็ตามvirtualจะแนะนำโต๊ะเสมือนซึ่งต้องการค่าใช้จ่ายต่อพื้นที่วัตถุที่ไม่จำเป็นหรือไม่ต้องการในหลาย ๆ สถานการณ์ (ส่วนใหญ่?)

การใช้งานแบบtoStringไม่จริงจะไร้ประโยชน์เพราะสิ่งเดียวที่สามารถส่งคืนได้คือที่อยู่ของวัตถุซึ่งเป็นผู้ใช้ที่ไม่เป็นมิตรมากและผู้โทรเข้าใช้งานแล้วซึ่งแตกต่างจากใน Java
ในทำนองเดียวกัน nonvirtual equalsหรือhashCodeสามารถใช้ที่อยู่เพื่อเปรียบเทียบวัตถุซึ่งค่อนข้างไร้ประโยชน์อีกครั้งและมักผิดอย่างสิ้นเชิง - ซึ่งแตกต่างจากใน Java วัตถุจะถูกคัดลอกบ่อยครั้งใน C ++ และด้วยเหตุนี้การจำแนก "เอกลักษณ์" ของวัตถุจึงไม่เหมือนกัน มีความหมายหรือมีประโยชน์เสมอ (เช่นตัวintจริงไม่ควรมีตัวตนนอกเหนือจากค่า ... จำนวนเต็มสองจำนวนที่มีค่าเดียวกันควรเท่ากัน)


ในความสัมพันธ์กับปัญหานี้และปัญหาระดับพื้นฐานที่เปราะบางที่บันทึกโดย Mike Nakis ให้สังเกตงานวิจัยที่น่าสนใจ / เสนอให้แก้ไขใน Java โดยทั่วไปโดยการทำทุกวิธีภายใน (เช่นเมื่อเรียกจากคลาสเดียวกัน) ไม่ใช่เสมือน แต่รักษาพฤติกรรมเสมือนเมื่อ ภายนอกเรียกว่า; เพื่อรับพฤติกรรมเก่า / มาตรฐาน (เช่นเสมือนจริงทุกที่) ข้อเสนอแนะนำopenคำหลักใหม่ ฉันไม่คิดว่ามันจะไปที่ใดนอกเหนือจากกระดาษ
Fizz

สามารถดูการอภิปรายเพิ่มเติมเกี่ยวกับกระดาษแผ่นนั้นได้ที่lambda-the-ultimate.org/classic/message12271.html
Fizz

มีระดับฐานร่วมกันจะทำให้มันเป็นไปได้ในการทดสอบใด ๆ shared_ptr<Foo>เพื่อดูว่ามันเป็นยังมีshared_ptr<Bar>(หรือในทำนองเดียวกันกับชนิดตัวชี้อื่น ๆ ) แม้ว่าFooและBarชั้นเรียนที่ไม่เกี่ยวข้องที่รู้อะไรเกี่ยวกับแต่ละอื่น ๆ ต้องการให้สิ่งนั้นทำงานร่วมกับ "ตัวชี้แบบดิบ" เนื่องจากประวัติของวิธีการใช้สิ่งนั้นจะมีราคาแพง แต่สำหรับสิ่งที่จะถูกจัดเก็บเป็นกองอยู่ดีค่าใช้จ่ายเพิ่มเติมจะน้อยมาก
supercat

แม้ว่ามันอาจจะไม่มีประโยชน์ที่จะมีคลาสพื้นฐานร่วมกันสำหรับทุกสิ่ง แต่ฉันคิดว่ามีบางประเภทของวัตถุที่ค่อนข้างใหญ่ซึ่งคลาสพื้นฐานทั่วไปจะมีประโยชน์ ตัวอย่างเช่นคลาสจำนวนมาก (ส่วนใหญ่เป็นจำนวนมากหากไม่ใช่เสียงส่วนใหญ่) ใน Java อาจถูกนำมาใช้ในสองวิธี: ในฐานะผู้ถือข้อมูลที่ไม่แน่นอนและไม่สามารถแชร์หรือเป็นผู้ถือข้อมูลที่ไม่มีใครสามารถแก้ไข ด้วยรูปแบบการใช้งานทั้งสองจะใช้ตัวชี้ที่มีการจัดการ (ข้อมูลอ้างอิง) เป็นพร็อกซีสำหรับข้อมูลพื้นฐาน ความสามารถในการมีชนิดตัวชี้ที่จัดการทั่วไปสำหรับข้อมูลดังกล่าวทั้งหมดมีประโยชน์
supercat

16

การมีออบเจ็กต์หนึ่งจะ จำกัด สิ่งที่คุณสามารถทำได้และสิ่งที่คอมไพเลอร์สามารถทำได้โดยไม่ต้องจ่ายผลตอบแทนมากนัก

คลาสรูททั่วไปทำให้สามารถสร้าง container-of-nothing และแยกสิ่งที่พวกมันมีdynamic_castแต่ถ้าคุณต้องการ container-of-nothing อะไรก็คล้ายกับที่จะboost::anyทำได้โดยไม่มีคลาสรูททั่วไป และboost::anyยังรองรับพื้นฐาน - มันยังสามารถรองรับการเพิ่มประสิทธิภาพบัฟเฟอร์ขนาดเล็กและปล่อยให้พวกเขาเกือบ "unboxed" ในสำนวนภาษาจาวา

C ++ รองรับและเจริญเติบโตในประเภทของค่า ทั้งตัวอักษรและโปรแกรมเมอร์ประเภทค่าเขียน คอนเทนเนอร์ C ++ จัดเก็บเรียงลำดับแฮชบริโภคและผลิตประเภทมูลค่าอย่างมีประสิทธิภาพ

การสืบทอดโดยเฉพาะอย่างยิ่งชนิดของคลาสฐานสไตล์ Java ที่ถ่ายทอดทางพันธุกรรมหมายความว่าต้องใช้ "ตัวชี้" หรือ "อ้างอิง" ประเภทร้านค้าฟรี หมายเลขอ้างอิง / ตัวชี้ / การอ้างอิงถึงข้อมูลของคุณมีตัวชี้ไปยังอินเทอร์เฟซของคลาสและ polymorphically สามารถแสดงสิ่งอื่นได้

แม้ว่าสิ่งนี้จะมีประโยชน์ในบางสถานการณ์เมื่อคุณแต่งงานกับรูปแบบที่มี "คลาสพื้นฐานทั่วไป" คุณได้ล็อคฐานรหัสทั้งหมดของคุณเป็นค่าใช้จ่ายและน้ำหนักกระเป๋าของรูปแบบนี้แม้ว่าจะไม่มีประโยชน์ก็ตาม

เกือบทุกครั้งที่คุณรู้เกี่ยวกับประเภทมากกว่า "เป็นวัตถุ" ที่ไซต์การโทรหรือในรหัสที่ใช้

ถ้าฟังก์ชั่นนั้นง่ายการเขียนฟังก์ชั่นเป็นเทมเพลตจะช่วยให้คุณมีรูปแบบการรวบรวมตามเวลาที่หลากหลายซึ่งข้อมูลในไซต์ที่เรียกไม่ถูกโยนทิ้งไป หากฟังก์ชันมีความซับซ้อนมากขึ้นการลบประเภทสามารถทำได้โดยการดำเนินการแบบสม่ำเสมอในประเภทที่คุณต้องการดำเนินการ (เช่นการทำให้เป็นอนุกรมและการลดทอนความเป็นไปได้) สามารถสร้างและจัดเก็บได้ (ณ เวลารวบรวม) รหัสในหน่วยการแปลอื่น

สมมติว่าคุณมีห้องสมุดบางแห่งที่คุณต้องการให้ทุกอย่างเป็นอนุกรม วิธีหนึ่งคือการมีคลาสฐาน:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

serialization_friendlyตอนนี้บิตของรหัสทุกครั้งที่คุณสามารถเขียน

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}

ยกเว้นไม่ใช่กstd::vectorดังนั้นตอนนี้คุณต้องเขียนทุกคอนเทนเนอร์ และไม่ใช่จำนวนเต็มที่คุณได้รับจากห้องสมุด bignum นั้น และไม่ใช่ประเภทที่คุณเขียนว่าคุณไม่คิดว่าจำเป็นต้องมีการทำให้เป็นอนุกรม และไม่ได้เป็นtupleหรือintหรือหรือdoublestd::ptrdiff_t

เราใช้แนวทางอื่น:

void write_to( my_buffer* b, int x ) {
  b->write_integer(x);
}    
template<class T,
  class=std::enable_if_t< void_t<
    std::declval<T const*>()->write_to( std::declval<my_buffer*>()
  > >
>
void write_to( my_buffer* b, T const* x ) {
  if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
  write_to( b, t );
}

ซึ่งประกอบไปด้วยดีไม่ทำอะไรเลยดูเหมือน ยกเว้นตอนนี้เราสามารถขยายได้write_toโดยการแทนที่write_toเป็นฟังก์ชันฟรีในเนมสเปซของประเภทหรือวิธีการในประเภท

เรายังสามารถเขียนโค้ดการลบประเภท:

namespace details {
  struct can_serialize_pimpl {
    virtual void write_to( my_buffer* ) const = 0;
    virtual void read_from( my_buffer const* ) = 0;
    virtual ~can_serialize_pimpl() {}
  };
}
struct can_serialize {
  void write_to( my_buffer* b ) const { pImpl->write_to(b); }
  void read_from( my_buffer const* b ) { pImpl->read_from(b); }
  std::unique_ptr<details::can_serialize_pimpl> pImpl;
  template<class T> can_serialize(T&&);
};
namespace details { 
  template<class T>
  struct can_serialize : can_serialize_pimpl {
    std::decay_t<T>* t;
    void write_to( my_buffer*b ) const final override {
      serialize( b, std::forward<T>(*t) );
    }
    void read_from( my_buffer const* ) final override {
      deserialize( b, std::forward<T>(*t) );
    }
    can_serialize(T&& in):t(&in) {}
  };
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
  std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}

และตอนนี้เราสามารถนำประเภทใดก็ได้และใส่เข้าไปในcan_serializeอินเทอร์เฟซที่ช่วยให้คุณสามารถเรียกใช้serializeในภายหลังผ่านอินเตอร์เฟสเสมือน

ดังนั้น:

void writer_thingy( can_serialize s );

เป็นฟังก์ชันที่ใช้ทุกสิ่งที่สามารถทำให้เป็นอันดับแทน

void writer_thingy( serialization_friendly const* s );

และครั้งแรกซึ่งแตกต่างจากที่สองก็สามารถจัดการintได้std::vector<std::vector<Bob>>โดยอัตโนมัติ

มันใช้เวลาไม่นานในการเขียนโดยเฉพาะอย่างยิ่งเพราะสิ่งนี้เป็นสิ่งที่คุณไม่ค่อยอยากทำ แต่เราได้รับความสามารถในการรักษาสิ่งต่าง ๆ ให้เป็นอนุกรมโดยไม่ต้องใช้ประเภทฐาน

มีอะไรมากกว่าที่ตอนนี้เราสามารถทำให้std::vector<T>serializable ในฐานะพลเมืองชั้นแรกโดยเพียงแค่การเอาชนะwrite_to( my_buffer*, std::vector<T> const& )ด้วย - เกินนั้นก็สามารถส่งผ่านไปยังcan_serializeและ serializabilty ของstd::vectorได้รับการจัดเก็บไว้ใน vtable .write_toและเข้าถึงได้โดย

ในระยะสั้น C ++ มีพลังมากพอที่คุณจะสามารถใช้ประโยชน์จากคลาสฐานเดียวแบบทันทีเมื่อต้องการโดยไม่ต้องจ่ายราคาของลำดับชั้นการสืบทอดที่บังคับเมื่อไม่จำเป็น และเวลาที่จำเป็นต้องใช้ฐานเดียว (แกล้งหรือไม่) นั้นหายาก

เมื่อประเภทเป็นตัวตนของพวกเขาและคุณรู้ว่าพวกเขาคืออะไรโอกาสในการปรับให้เหมาะสมนั้นมีอยู่มากมาย ข้อมูลถูกเก็บไว้ในเครื่องและต่อเนื่องกัน (ซึ่งเป็นสิ่งสำคัญอย่างยิ่งสำหรับแคชที่เป็นมิตรกับโปรเซสเซอร์ที่ทันสมัย) คอมไพเลอร์สามารถเข้าใจได้อย่างง่ายดายว่าการดำเนินการที่กำหนดไว้คืออะไร (แทนที่จะมีตัวชี้เมธอดเสมือนทึบแสงที่ต้องกระโดดข้าม อีกด้านหนึ่ง) ซึ่งช่วยให้สามารถเรียงลำดับคำสั่งได้ดีที่สุดและตอกหมุดลงไปในรูกลม


8

มีคำตอบที่ดีหลายข้อข้างต้นและความจริงที่ชัดเจนว่าสิ่งใดก็ตามที่คุณจะทำกับฐานของวัตถุทั้งหมดสามารถทำได้ดีกว่าในรูปแบบอื่น ๆ ดังที่แสดงโดยคำตอบของ @ ratchetfreak และความคิดเห็นเกี่ยวกับมันเป็นสิ่งสำคัญมาก มีอีกเหตุผลหนึ่งที่ทำให้คุณไม่ต้องสร้างมรดกทางเพชรเมื่อมีการใช้หลายมรดก หากคุณมีฟังก์ชั่นใด ๆ ในคลาสพื้นฐานสากลทันทีที่คุณเริ่มใช้การสืบทอดหลายครั้งคุณจะต้องเริ่มต้นระบุตัวแปรที่คุณต้องการเข้าถึงเนื่องจากมันสามารถโหลดมากเกินไปแตกต่างกันในเส้นทางที่แตกต่างกันของห่วงโซ่การสืบทอด และฐานไม่สามารถเป็นเสมือนได้เนื่องจากจะไม่มีประสิทธิภาพมาก (ต้องการให้วัตถุทั้งหมดมีตารางเสมือนจริงที่มีค่าใช้จ่ายมหาศาลในการใช้หน่วยความจำและพื้นที่ใกล้เคียง) นี่จะกลายเป็นฝันร้ายทางลอจิสติกส์อย่างรวดเร็ว


1
วิธีหนึ่งในการแก้ไขปัญหาเพชรคือการมีทุกประเภทที่ไม่ได้มาจากประเภทฐานผ่านหลายเส้นทางแทนที่สมาชิกเสมือนทั้งหมดของประเภทฐานนั้น ถ้าชนิดพื้นฐานทั่วไปถูกสร้างขึ้นในภาษาตั้งแต่เริ่มต้นคอมไพเลอร์สามารถสร้างการใช้งานเริ่มต้นที่ถูกต้องตามกฎหมายอัตโนมัติ (แต่ไม่จำเป็นต้องน่าประทับใจ)
supercat

5

ในความเป็นจริง Microsofts ต้น C ++ คอมไพเลอร์และไลบรารี (ฉันรู้เกี่ยวกับ Visual C ++, 16 บิต) มีคลาสชื่อCObjectเช่นนี้

อย่างไรก็ตามคุณต้องรู้ว่าในขณะนั้น "เทมเพลต" ไม่ได้รับการสนับสนุนโดยคอมไพเลอร์ C ++ แบบง่ายนี้จึงstd::vector<class T>ไม่สามารถใช้คลาสเช่นนี้ได้ แต่การใช้ "เวกเตอร์" นั้นสามารถจัดการชั้นเรียนประเภทเดียวได้ดังนั้นจึงมีชั้นเรียนที่สามารถเทียบเคียงได้ในstd::vector<CObject>ปัจจุบัน เนื่องจากCObjectเป็นคลาสพื้นฐานของเกือบทุกคลาส (น่าเสียดายที่ไม่เท่ากับCString- stringในคอมไพเลอร์สมัยใหม่) คุณสามารถใช้คลาสนี้เพื่อจัดเก็บวัตถุเกือบทุกชนิด

เนื่องจากคอมไพเลอร์สมัยใหม่รองรับเทมเพลตการใช้งานกรณีนี้ของ "คลาสพื้นฐานทั่วไป" ไม่ได้รับอีกต่อไป

คุณต้องคิดเกี่ยวกับความจริงที่ว่าการใช้คลาสพื้นฐานดังกล่าวจะมีค่าใช้จ่าย (เล็กน้อย) หน่วยความจำและรันไทม์ - ตัวอย่างเช่นในการเรียกไปยังตัวสร้าง ดังนั้นจึงมีข้อเสียเมื่อใช้คลาสดังกล่าว แต่อย่างน้อยเมื่อใช้คอมไพเลอร์ C ++ ที่ทันสมัยจึงแทบไม่มีกรณีการใช้งานสำหรับคลาสดังกล่าว


3
MFC นั้นใช่ไหม [ความคิดเห็นที่
แพ็ดดิ้ง

3
มันเป็น MFC อย่างแน่นอน สัญญาณที่เปล่งประกายของการออกแบบ OO ที่แสดงให้โลกเห็นว่าควรทำอย่างไร โอ้รอ ...
gbjbaanb

4
@gbjbaanb Turbo Pascal และ Turbo C ++ มีของตัวเองTObjectก่อนที่ MFC จะมีอยู่จริง อย่าตำหนิ Microsoft สำหรับส่วนหนึ่งของการออกแบบมันเป็นความคิดที่ดีที่ทุกคนในเวลานั้น
hvd

แม้กระทั่งก่อนหน้าเทมเพลตการพยายามเขียน Smalltalk ใน C ++ ให้ผลลัพธ์ที่น่ากลัว
JDługosz

@hvd ยังคง MFC เป็นตัวอย่างที่แย่กว่ามากในการออกแบบเชิงวัตถุมากกว่าที่ Borland ผลิตออกมา
จูลส์

5

ฉันจะแนะนำอีกเหตุผลที่มาจาก Java

เพราะคุณไม่สามารถสร้างคลาสพื้นฐานสำหรับทุกอย่างเป็นอย่างน้อยไม่ได้หากไม่มีแผ่นหม้อไอน้ำ

คุณอาจหนีไปกับคลาสของคุณเอง - แต่คุณอาจพบว่าคุณทำซ้ำรหัสจำนวนมาก เช่น "ฉันไม่สามารถใช้งานstd::vectorที่นี่ได้เนื่องจากไม่ได้ใช้งานIObject- ฉันควรสร้างสิ่งที่ได้มาใหม่IVectorObjectที่ทำในสิ่งที่ถูกต้อง ... "

จะเป็นกรณีนี้เมื่อใดก็ตามที่คุณจัดการกับ built-in หรือไลบรารีคลาสมาตรฐานหรือคลาสจากไลบรารีอื่น

ตอนนี้ถ้ามันถูกสร้างขึ้นในภาษาที่คุณจะต้องจบลงด้วยสิ่งต่าง ๆ เช่นIntegerและintความสับสนที่อยู่ในจาวาหรือการเปลี่ยนแปลงใหญ่ในไวยากรณ์ภาษา (ใจคุณฉันคิดว่าภาษาอื่น ๆ ทำได้ดีมากด้วยการสร้างมันลงในทุกประเภท - ทับทิมดูเหมือนเป็นตัวอย่างที่ดีกว่า)

โปรดทราบด้วยว่าหากคลาสพื้นฐานของคุณไม่ใช่ polymorphic แบบรันไทม์ (เช่นการใช้ฟังก์ชั่นเสมือนจริง) คุณจะได้รับประโยชน์แบบเดียวกันจากการใช้คุณลักษณะเช่นกรอบงาน

เช่นแทนที่จะเป็น.toString()คุณอาจมีสิ่งต่อไปนี้: (หมายเหตุ: ฉันรู้ว่าคุณสามารถทำ neater นี้โดยใช้ห้องสมุดที่มีอยู่ ฯลฯ มันเป็นเพียงตัวอย่างที่เป็นตัวอย่าง)

template<typename T>
struct ToStringTrait;

template<typename T> 
std::string toString(const T & t) {
  return ToStringTrait<T>::toString(t);
}

template<>
struct ToStringTrait<int> {
  std::string toString(int v) {
    return itoa(v);
  }
}

template<typename T>
struct ToStringTrait<std::vector<T>> {
  std::string toString(const std::vector<T> &v) {
    std::stringstream ss;
    ss<<"{";
    for(int i=0; i<v.size(); ++i) {
      ss<<toString(v[i]);
    }
    ss<<"}";
    return ss.str();
  }
}

3

arguably "โมฆะ" ตอบสนองบทบาทของคลาสฐานสากลมากมาย คุณสามารถโยนตัวชี้ใด ๆ void*ไป จากนั้นคุณสามารถเปรียบเทียบตัวชี้เหล่านั้น คุณสามารถstatic_castกลับไปที่คลาสเดิมได้

แต่สิ่งที่คุณไม่สามารถทำอะไรกับvoidที่คุณสามารถทำได้ด้วยObjectการใช้ RTTI จะคิดออกว่าชนิดของวัตถุที่คุณมีจริงๆ ท้ายที่สุดนี่คือสิ่งที่วัตถุทั้งหมดใน C ++ มี RTTI และท้ายที่สุดก็เป็นไปได้ที่จะมีวัตถุที่มีความกว้างเป็นศูนย์


1
subobjects baseclass ที่มีความกว้างเป็นศูนย์เท่านั้นไม่ใช่แบบปกติ
Deduplicator

@Dupuplicator ตามการปรับปรุง C ++ 17 จะเพิ่ม[[no_unique_address]]ซึ่งคอมไพเลอร์สามารถใช้เพื่อกำหนดความกว้างศูนย์ให้กับ subobjects ของสมาชิก
underscore_d

1
@underscore_d คุณหมายถึงการวางแผนสำหรับ C ++ 20 [[no_unique_address]]จะอนุญาตให้คอมไพเลอร์แก่สมาชิก EBO ตัวแปร
Deduplicator

@Deduplicator อ๊ะ, yup ฉันเริ่มใช้ C ++ 17 แล้ว แต่ฉันคิดว่าฉันยังคิดว่ามันล้ำสมัยกว่าที่เป็นจริง!
underscore_d

2

Java ใช้ปรัชญาการออกแบบที่ไม่ควรมีพฤติกรรมที่ไม่ได้กำหนด รหัสเช่น:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();

จะทดสอบว่าfelixมีชนิดย่อยของCatอินเตอร์เฟสที่ใช้Wooferหรือไม่ ถ้ามันจะทำการแสดงและเรียกใช้woof()และถ้าไม่มันจะโยนข้อยกเว้น ลักษณะการทำงานของรหัสที่ถูกกำหนดไว้อย่างเต็มที่ไม่ว่าจะfelixดำเนินการWooferหรือไม่

C ++ ยึดถือปรัชญาว่าหากโปรแกรมไม่ควรพยายามดำเนินการบางอย่างมันไม่สำคัญว่ารหัสที่สร้างขึ้นจะทำอย่างไรหากการดำเนินการดังกล่าวเกิดขึ้นและคอมพิวเตอร์ไม่ควรเสียเวลาในการพยายาม จำกัด พฤติกรรมในกรณีที่ "ควร" ไม่เคยเกิดขึ้น ใน C ++ ให้เพิ่มตัวดำเนินการทางอ้อมที่เหมาะสมเพื่อที่จะส่ง*Catไปยัง a *Wooferโค้ดจะให้ผลการทำงานที่กำหนดไว้เมื่อการส่งนั้นถูกต้องตามกฎหมายแต่ไม่ได้กำหนดพฤติกรรมที่ไม่ได้กำหนดไว้

การมีประเภทพื้นฐานร่วมกันสำหรับสิ่งต่าง ๆ ทำให้สามารถตรวจสอบการปลดเปลื้องระหว่างอนุพันธ์ของประเภทฐานนั้นและการดำเนินการทดลองใช้งานได้ แต่การตรวจสอบความถูกต้องของการแสดงนั้นแพงกว่าเพียงแค่สมมติว่าพวกเขาถูกกฎหมายและหวังว่าจะไม่มีอะไรเลวร้ายเกิดขึ้น ปรัชญาของซีพลัสพลัสคือการตรวจสอบดังกล่าวต้องใช้ "การจ่ายเงินเพื่อสิ่งที่คุณ [ปกติ] ไม่ต้องการ"

อีกปัญหาหนึ่งที่เกี่ยวข้องกับ C ++ แต่จะไม่เป็นปัญหาสำหรับภาษาใหม่คือถ้าโปรแกรมเมอร์หลายคนสร้างฐานร่วมให้สืบทอดคลาสของตนเองจากนั้นและเขียนโค้ดเพื่อทำงานกับสิ่งต่าง ๆ ของคลาสพื้นฐานทั่วไปนั้น รหัสดังกล่าวจะไม่สามารถทำงานกับวัตถุที่พัฒนาโดยโปรแกรมเมอร์ซึ่งใช้คลาสพื้นฐานที่แตกต่างกัน หากภาษาใหม่ต้องการให้วัตถุฮีปทั้งหมดมีรูปแบบส่วนหัวร่วมกันและไม่เคยอนุญาตให้ใช้กับวัตถุฮีปซึ่งไม่ได้ใช้ดังนั้นวิธีการที่ต้องอ้างอิงถึงวัตถุฮีปที่มีส่วนหัวดังกล่าวจะยอมรับการอ้างอิงไปยังวัตถุฮีปทุกคน สามารถสร้างได้

โดยส่วนตัวแล้วฉันคิดว่าการมีวิธีการทั่วไปในการถามวัตถุ "คุณสามารถเปลี่ยนเป็นประเภท X" ได้หรือไม่เป็นคุณลักษณะที่สำคัญมากในภาษา / กรอบงาน แต่ถ้าคุณลักษณะดังกล่าวไม่ได้สร้างเป็นภาษาตั้งแต่เริ่มต้น เพิ่มในภายหลัง โดยส่วนตัวแล้วฉันคิดว่าควรเพิ่มคลาสพื้นฐานดังกล่าวในไลบรารีมาตรฐานในโอกาสแรกด้วยคำแนะนำที่รัดกุมว่าวัตถุทั้งหมดที่จะใช้ polymorphically ควรสืบทอดจากฐานนั้น การมีโปรแกรมเมอร์แต่ละคนใช้ "ประเภทพื้นฐาน" ของตัวเองจะทำให้การส่งผ่านวัตถุระหว่างรหัสของคนอื่นยากขึ้น แต่การมีประเภทพื้นฐานร่วมกันซึ่งโปรแกรมเมอร์จำนวนมากที่สืบทอดมาจะทำให้ง่ายขึ้น

ภาคผนวก

ใช้เทมเพลตเป็นไปได้ที่จะกำหนด "ผู้ถือวัตถุโดยพลการ" และถามเกี่ยวกับประเภทของวัตถุที่อยู่ในนั้น แพคเกจ Boost anyมีสิ่งที่เรียกว่า ดังนั้นแม้ว่า C ++ จะไม่มีการอ้างอิงแบบ "ชนิดที่ตรวจสอบได้กับชนิดใดก็ได้" แต่ก็เป็นไปได้ที่จะสร้างขึ้นมา สิ่งนี้ไม่ได้แก้ปัญหาที่เกิดขึ้นเมื่อไม่มีสิ่งใดในมาตรฐานภาษาเช่นความไม่ลงรอยกันระหว่างการใช้งานของโปรแกรมเมอร์ที่แตกต่างกัน แต่มันอธิบายว่า C ++ ได้รับโดยไม่ต้องมีประเภทพื้นฐานที่ทุกอย่างมาจาก: โดยทำให้เป็นไปได้ สิ่งที่ทำหน้าที่เหมือนหนึ่ง


หล่อที่ล้มเหลวที่รวบรวมเวลาในC ++ , JavaและC #
milleniumbug

1
@milleniumbug: ถ้าWooferเป็นอินเตอร์เฟซและCatเป็นที่สืบทอดหล่อจะถูกต้องตามกฎหมายเพราะอาจมีอยู่ (ถ้าไม่ได้ตอนนี้อาจจะเป็นในอนาคตบริการ) WoofingCatซึ่งสืบทอดมาจากและการดำเนินการCat Wooferโปรดทราบว่าภายใต้ Java รวบรวม / เชื่อมโยงรูปแบบการสร้างWoofingCatจะไม่จำเป็นต้องเข้าถึงซอร์สโค้ดสำหรับมิได้Cat Woofer
supercat

3
C ++ มีdynamic_castซึ่งถูกจับพยายามที่จะโยนจากCatไปWooferและจะตอบคำถามที่ว่า "คุณแปลงสภาพที่จะพิมพ์ X" C ++ จะช่วยให้คุณบังคับนักแสดงทำให้เกิดบางทีคุณอาจรู้จริง ๆ ว่าคุณกำลังทำอะไร แต่มันจะช่วยคุณได้ถ้านั่นไม่ใช่สิ่งที่คุณตั้งใจจะทำ
Rob K

2
@RobK: ถูกต้องเกี่ยวกับไวยากรณ์แน่นอน กฟน. ฉันได้อ่านเพิ่มเติมเกี่ยวกับ dynamic_cast และดูเหมือนว่า c ++ สมัยใหม่มีวัตถุ polymorphic ทั้งหมดที่ได้มาจากฐาน "polymorphic object" คลาสพื้นฐานที่มีฟิลด์อะไรก็ตามที่จำเป็นในการระบุประเภทของวัตถุ (โดยทั่วไปจะเป็น vtable ตัวชี้แม้ว่าจะเป็นรายละเอียดการนำไปใช้งาน) C ++ ไม่ได้อธิบายชั้นเรียน polymorphic ว่าวิธีการ แต่ผ่านตัวชี้ไปยังdynamic_castจะได้กำหนดพฤติกรรมถ้ามันชี้ไปยังวัตถุ polymorphic และพฤติกรรมที่ไม่ได้กำหนดถ้ามันไม่ได้ดังนั้นจากมุมมองความหมาย ...
SuperCat

2
... วัตถุ polymorphic ทั้งหมดเก็บข้อมูลบางส่วนในรูปแบบเดียวกันและทั้งหมดสนับสนุนพฤติกรรมที่ไม่ได้รับการสนับสนุนโดยวัตถุที่ไม่ใช่ polymorphic ในใจของฉันนั่นหมายความว่าพวกเขาประพฤติตนราวกับว่าพวกเขาได้รับมาจากฐานร่วมไม่ว่าคำนิยามภาษาจะใช้คำศัพท์ดังกล่าวหรือไม่
SuperCat

1

ในความเป็นจริงแล้ว Symbian C ++ นั้นมีคลาสพื้นฐานสากลคือ CBase สำหรับวัตถุทั้งหมดที่ทำงานในลักษณะเฉพาะ (ส่วนใหญ่ถ้าพวกเขาต้องการจัดสรรฮีป) มันให้ destructor เสมือนศูนย์ความทรงจำของการก่อสร้างและซ่อนตัวสร้างสำเนา

เหตุผลเบื้องหลังคือมันเป็นภาษาสำหรับระบบฝังตัวและคอมไพเลอร์ C ++ และสเปคนั้นอึจริงเมื่อ 10 ปีที่แล้ว

บางคลาสที่สืบทอดมาจากนี้มีเพียงบางคลาสเท่านั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.