2009-07-05 42 views
5

Giả sử Y là một lớp dẫn xuất từ ​​lớp X và X khai báo foo là ảo. Giả sử y là kiểu (Y *). Sau đó ((X *) y) -> foo() sẽ thực hiện phiên bản Y của foo(), nhưng ((X) * y) .foo() sẽ thực thi phiên bản X. Bạn có thể cho tôi biết tại sao đa hình không áp dụng trong trường hợp không quan tâm? Tôi mong đợi một trong hai cú pháp sẽ mang lại phiên bản Y của foo().C++ đa hình ((X *) y) -> foo() vs ((X) * y) .foo()

Trả lời

0

Tôi tin điều đó đơn giản là do cách ngôn ngữ được chỉ định. Tài liệu tham khảo và con trỏ sử dụng ràng buộc trễ bất cứ nơi nào có thể, trong khi các đối tượng sử dụng ràng buộc sớm. Nó sẽ có thể làm cuối ràng buộc trong mọi trường hợp (tôi tưởng tượng), nhưng một trình biên dịch đã làm điều đó sẽ không được theo các thông số kỹ thuật C + +.

8

Một diễn viên luôn (*) tạo đối tượng mới của loại bạn đang truyền đến, được tạo bằng cách sử dụng đối tượng bạn đang truyền.

Truyền tới X * tạo ra một con trỏ mới (tức là, một đối tượng kiểu X *). Nó có cùng giá trị là y, vì vậy nó vẫn trỏ đến cùng một đối tượng, thuộc loại Y.

Đúc thành X tạo ra một X. Nó được xây dựng bằng cách sử dụng *y, nhưng không có gì liên quan đến đối tượng cũ . Trong ví dụ của bạn, foo() được gọi trên đối tượng "tạm thời" mới này, không phải trên đối tượng được trỏ đến bởi y.

Bạn chính xác rằng đa hình động chỉ áp dụng cho con trỏ và tham chiếu, không cho đối tượng và đây là lý do tại sao: nếu bạn có con trỏ đến X thì điều nó trỏ đến có thể là phân lớp của X. Nhưng nếu bạn có chữ X, thì đó là chữ X và không có gì khác. các cuộc gọi ảo sẽ là vô nghĩa.

(*) trừ khi tối ưu hóa cho phép bỏ sót mã không thay đổi kết quả. Nhưng tối ưu hóa không được phép thay đổi chức năng foo() được gọi.

+1

... và ((X &) * y) .foo() _does_ gọi hàm foo có nguồn gốc, bởi vì truyền Y & đến X & không tạo ra X. – Doug

+0

Có, tôi có lẽ nên nói "luôn tạo đối tượng mới của loại bạn đang truyền tới, trừ khi loại bạn đang truyền tới không phải là loại đối tượng". Truyền thành kiểu tham chiếu "tạo" một tham chiếu mới được ràng buộc với đối tượng gốc. –

0

Tôi nghĩ rằng lời giải thích Darth Eru là chính xác, và đây là lý do tại sao tôi nghĩ rằng C++ cư xử theo cách đó:

Mã (X) * y cũng giống như tạo ra một biến địa phương mà là loại X. Trình biên dịch cần phân bổ sizeof (X) không gian trên stack, và nó ném đi bất kỳ dữ liệu phụ bao gồm trong một đối tượng của loại Y, vì vậy khi bạn gọi foo() nó có để thực hiện phiên bản X. Sẽ rất khó cho trình biên dịch hoạt động theo cách cho phép bạn gọi phiên bản Y.

Mã (X *) y giống như tạo con trỏ đến đối tượng và trình biên dịch biết rằng đối tượng được trỏ đến là X hoặc phân lớp của X. Khi chạy dereference con trỏ và gọi foo bằng "- > foo() "lớp của đối tượng được xác định và sử dụng đúng chức năng.

+0

Hãy nhớ rằng (X) * y được định nghĩa giống như X (* y), tức là một cuộc gọi đến một số hàm tạo X để tạo một đối tượng X mới. Nó sẽ không chỉ là khó khăn, nó thậm chí không mong muốn cho một đối tượng X có phiên bản Y của foo() gọi nó, chỉ vì nó xảy ra đã được xây dựng bằng cách sử dụng một đối tượng Y. –

2

Các dereferencing (phần *y) là tốt, nhưng các diễn viên (phần (X)) tạo ra một đối tượng mới (tạm thời) đặc biệt của lớp X - đó là những gì đúc nghĩa. Vì vậy, đối tượng phải có bảng ảo từ lớp X - hãy xem xét quá trình truyền sẽ xóa bất kỳ thành phần cá thể nào được thêm bởi Y trong phân lớp con (thực sự, làm sao có thể biết được bản sao của X có thể biết về chúng?) có khả năng sẽ là thảm họa nếu bất kỳ phần ghi đè nào của Y được thực thi - bảo mật trong kiến ​​thức của họ rằng this trỏ đến một phiên bản Y, hoàn thành với các thành viên bổ sung và tất cả ... khi kiến ​​thức đó là sai!

Phiên bản mà bạn đúc con trỏ là dĩ nhiên hoàn toàn khác nhau - *X chỉ có các bit giống như Y*, vì vậy nó vẫn trỏ đến một trường hợp hoàn toàn hợp lệ của Y (trên thực tế, nó trỏ đến y, tất nhiên).

Thực tế đáng buồn là, vì sự an toàn, ctor bản sao của một lớp nên thực sự chỉ được gọi với, như đối số, một thể hiện của lớp đó - không phải của bất kỳ phân lớp nào; sự mất mát của các thành viên thể hiện thêm & c là quá gây tổn hại. Nhưng cách duy nhất để đảm bảo đó là tuân theo lời khuyên tuyệt vời của Haahr, "Đừng phân lớp các lớp cụ thể" ... mặc dù anh ta viết về Java, lời khuyên ít nhất cũng tốt cho C++ (trong đó có ctor sao chép này " cắt "vấn đề ngoài! -)

+0

Nếu mất các thành viên phụ của Y gây tổn hại khi sao chép X, thì lớp Y không thỏa mãn nguyên lý thay thế Liskov, hoặc nhiều khả năng người gọi đã không nhận ra rằng anh ta đang đúc (slice không mong muốn) và nghĩ rằng anh ta vẫn có đối tượng của lớp Y. Dù bằng cách nào, tôi không nghĩ rằng lỗi là, như vậy, gọi các nhà xây dựng bản sao với một Y. Nó không đánh giá cao rằng kết quả là một X được xây dựng từ Y như bất kỳ nhà xây dựng 1-arg khác và không phải bất cứ điều gì đa hình. –

+0

Phần không mong muốn là nguyên nhân có khả năng nhất, và nếu bạn không phân lớp các lớp cụ thể (ngoài những lý do khác Haahr cho) đó là một loại tai nạn không xảy ra! -) –

+0

Chắc chắn, đó là quy tắc âm thanh ngón tay cái. Tôi cũng đồng ý với Haahr có những trường hợp đáng để chance cánh tay của bạn, và phân lớp, bởi vì đôi khi thừa kế thực sự hữu ích. Nhưng nó phải thực sự có thể đáp ứng Liskov, đó là nơi mà hầu hết các lớp con của các lớp cụ thể đều sai. Nếu, như Haahr nói, * mọi khía cạnh * của siêu lớp được "kéo theo vô tình" thì bạn đã thất bại rồi. Nhưng bạn có thể phân lớp các lớp bê tông, trong trường hợp hiếm hoi nó có ý nghĩa. Nếu vậy, ctor sao chép có ý nghĩa quá, và những người hiểu những gì slicing là sẽ tránh nó anyway, bằng cách không làm điều đó. –

10

Bạn đang cắt phần đối tượng Y và sao chép đối tượng vào đối tượng X. Hàm này sau đó được gọi là được gọi trên đối tượng X và do đó chức năng của X được gọi.

Khi bạn chỉ định một loại trong C++ trong khai báo hoặc truyền, có nghĩa là để nói rằng đối tượng được khai báo hoặc được đưa vào thực tế thuộc loại đó, không phải của một kiểu dẫn xuất.

Nếu bạn muốn chỉ đơn thuần điều trị các đối tượng đang được loại X (có nghĩa là để nói, nếu bạn muốn loại tĩnh của biểu thức được X, nhưng vẫn muốn nó để biểu thị một đối tượng Y) sau đó bạn cast một tài liệu tham khảo loại

((X&)*y).foo() 

này sẽ gọi hàm trong đối tượng Y, và sẽ không cắt hay sao chép vào một đối tượng X. Theo các bước, điều này có nghĩa là

  • Dereference pointer y, là loại Y*. Dereferencing mang lại một biểu thức lvalue loại Y. Biểu thức lvalue thực sự có thể biểu thị một đối tượng của một loại có nguồn gốc, ngay cả khi loại tĩnh của nó là một trong các cơ sở của nó.
  • Truyền tới số X&, tham chiếu đến X. Điều đó sẽ mang lại một biểu thức lvalue loại X.
  • Gọi hàm.

dàn diễn viên ban đầu của bạn đã

  • dereference con trỏ y.
  • Biểu thức kết quả được đúc thành X. Điều này sẽ mang lại cho một hoạt động sao chép vào một đối tượng X mới. Biểu thức kết quả là biểu thức kiểu rvalue loại tĩnh X. Loại động của đối tượng được biểu thị là cũngX, giống như tất cả các biểu thức rvalue.
  • Gọi hàm.
Các vấn đề liên quan