2011-01-26 34 views
16

Tôi biết rằng C++ không hỗ trợ hiệp phương sai cho các phần tử vùng chứa, như trong Java hoặc C#. Vì vậy, đoạn mã sau có thể là hành vi không xác định:Hợp tác trung gian vùng chứa trong C++

#include <vector> 
struct A {}; 
struct B : A {}; 
std::vector<B*> test; 
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test); 

Không ngạc nhiên khi tôi nhận được giải pháp này khi đề xuất giải pháp này cho another question.

Nhưng phần nào của tiêu chuẩn C++ chính xác cho tôi biết rằng điều này sẽ dẫn đến hành vi không xác định? Nó đảm bảo rằng cả hai std::vector<A*>std::vector<B*> lưu trữ con trỏ của chúng trong một khối bộ nhớ. Nó cũng được đảm bảo rằng sizeof(A*) == sizeof(B*). Cuối cùng, A* a = new B là hoàn toàn hợp pháp.

Vì vậy, những gì xấu tinh thần trong tiêu chuẩn đã làm tôi gợi ý (ngoại trừ phong cách)?

+1

Việc sử dụng reinterpret_cast <>() sau đó không có gì được xác định. Nó có thể hoạt động nhưng danh sách các điều kiện của bạn quá ngắn ngủi. Tôi sẽ thêm một vài điều kiện trước. sizeof (A) == sizeof (B); Không phải A hoặc B có thể chứa bất kỳ loại chức năng ảo nào. Cả A và B cũng không phải bất kỳ hậu duệ nào được đặt trong mảng đều có thể sử dụng nhiều thừa kế. –

+7

Câu trả lời cụ thể không phải C++ là nó không an toàn.Nếu bạn thêm một 'A' vào foo bạn có kiểm tra trong trạng thái không hợp lệ vì nó đảm bảo rằng tất cả các phần tử có kiểu' B'. Và C# cũng không hỗ trợ điều này. C# chỉ hỗ trợ nó cho các tham số chung được sử dụng trong các cách an toàn (chỉ có đầu vào hoặc đầu ra) và chỉ trên các giao diện và các đại biểu. Java hỗ trợ nó bởi vì nó bổ sung kiểm tra thời gian chạy và làm việc nội bộ trên lớp cơ sở đối tượng. – CodesInChaos

+0

Câu hỏi này trông giống như http://stackoverflow.com/questions/842387/how-do-i-dynamically-cast-between-vectors-of-pointers – Nekuromento

Trả lời

16

Nguyên tắc vi phạm ở đây là tài liệu trong C++ 03 3.10/15 [basic.lval], trong đó quy định cụ thể những gì được gọi không chính thức là "quy tắc nghiêm ngặt răng cưa"

Nếu một chương trình cố gắng truy cập giá trị được lưu trữ của một đối tượng thông qua một vế trái của khác hơn là một trong các loại sau đây hành vi là undefined:

  • loại năng động của đối tượng,

  • một phiên bản cv-trình độ của các loại động của đối tượng,

  • một loại đó là ký kết hoặc kiểu unsigned tương ứng với các loại động của đối tượng,

  • một loại đó là ký kết hoặc kiểu unsigned tương ứng với một CV- phiên bản đủ của các loại động của đối tượng,

  • một loại tổng hợp hoặc công đoàn trong đó bao gồm một trong các loại nói trên giữa các thành viên của nó (bao gồm, đệ quy, thành viên của một subaggregate hoặc công đoàn chứa),

  • một loại mà là một (possi bly cv-đủ điều kiện) loại lớp cơ sở của loại động của đối tượng,

  • một loại char hoặc unsigned char.

Nói tóm lại, cho một đối tượng, bạn chỉ được phép truy cập vào đối tượng đó thông qua một biểu thức mà có một trong các loại trong danh sách. Đối với một đối tượng kiểu lớp không có các lớp cơ sở, chẳng hạn như std::vector<T>, về cơ bản bạn bị giới hạn ở các loại có tên trong dấu đầu tiên, thứ hai và cuối cùng.

std::vector<Base*>std::vector<Derived*> là các loại hoàn toàn không liên quan và bạn không thể sử dụng đối tượng thuộc loại std::vector<Base*> như thể nó là std::vector<Derived*>.Trình biên dịch có thể làm tất cả mọi thứ nếu bạn vi phạm quy định này, bao gồm:

  • thực hiện tối ưu hóa khác nhau trên một hơn bên kia, hoặc

  • bố trí các thành viên nội bộ của một cách khác nhau, hoặc

  • thực hiện tối ưu hóa giả định rằng một std::vector<Base*>* không bao giờ có thể tham khảo các đối tượng giống như một std::vector<Derived*>*

  • kiểm tra sử dụng thời gian chạy để đảm bảo t mũ bạn không vi phạm quy tắc bí danh nghiêm ngặt

[Nó cũng không đảm bảo rằng nó sẽ "hoạt động" và nếu bạn thay đổi trình biên dịch hoặc trình biên dịch các phiên bản hoặc cài đặt biên dịch, tất cả có thể ngừng "hoạt động". Tôi sử dụng dấu nháy mắt vì một lý do ở đây. :-)]

Thậm chí nếu bạn chỉ có Base*[N] bạn không thể sử dụng mảng đó như thể là Derived*[N] (mặc dù trong trường hợp đó, việc sử dụng có thể an toàn hơn, "an toàn hơn" có nghĩa là "vẫn chưa xác định nhưng ít hơn có khả năng giúp bạn có được vào rắc rối).

+0

@ James: cảm ơn vì đã đến ;-) Wow, nhận xét cuối cùng của bạn đến như là một bất ngờ thực sự đối với tôi. Tôi không coi bản thân mình là một người mới sử dụng C++ sau 15 năm làm việc với nó, nhưng tôi sẽ không bao giờ nghĩ rằng điều này (hành vi không hợp lệ) thậm chí được áp dụng trong trường hợp mảng! Cảm ơn. –

+0

Nó ('Base [N]' vs 'Derived [N]') sẽ giúp bạn chắc chắn gặp rắc rối nếu các kích thước khác nhau, ví dụ: nếu lớp dẫn xuất có nhiều thành viên hơn. – etarion

+0

@etarion: chúng ta đang nói các con trỏ ở đây: ví dụ, 'f (Base a []) {...}' và truyền nó (thông qua 'reinterpret_cast') một mảng' Derived'. –

4

bạn đang gọi tinh thần xấu của reinterpret_cast <>.

Trừ khi bạn thực sự biết những gì bạn làm (tôi có nghĩa là không tự hào và không vẻ thông thái) reinterpret_cast là một trong những cửa của số điện thoại

Cách sử dụng an toàn duy nhất mà tôi biết là quản lý lớp học es và cấu trúc giữa các hàm C++ và C gọi. Tuy nhiên, có thể có một số người khác.

+1

Sử dụng hợp lý khác là với phép tính xấp xỉ "toán nhanh" thúc đẩy sự biểu diễn của các số dấu phẩy động. Về cơ bản, rủi ro bị loại bỏ bởi các yêu cầu của các xấp xỉ, ví dụ: [quick inv sqrt] (https://en.wikipedia.org/wiki/Fast_inverse_square_root) mà (in-) nổi tiếng hoạt động độc quyền trên 'số dấu phẩy động 32 bit [s] trong định dạng dấu phẩy động IEEE 754'. TL; DR chỉ đạo rõ ràng trừ khi bạn là một chuyên gia "[mũi quỷ] (https://en.wikipedia.org/wiki/Undefined_behavior)" wrangler. –

2

Tôi nghĩ rằng nó sẽ được dễ dàng hơn để hiển thị hơn nói:

struct A { int a; }; 

struct Stranger { int a; }; 

struct B: Stranger, A {}; 

int main(int argc, char* argv[]) 
{ 
    B someObject; 
    B* b = &someObject; 

    A* correct = b; 
    A* incorrect = reinterpret_cast<A*>(b); 

    assert(correct != incorrect); // troubling, isn't it ? 

    return 0; 
} 

Các (cụ thể) vấn đề cho thấy ở đây là khi thực hiện chuyển đổi "đúng đắn", trình biên dịch cho biết thêm một số con trỏ ajdustement tùy thuộc vào bộ nhớ bố trí của các đối tượng. Trên reinterpret_cast, không có điều chỉnh nào được thực hiện.

Tôi cho rằng bạn sẽ hiểu tại sao việc sử dụng reinterpet_cast nên thường bị cấm từ mã ...

+0

Có, trong trường hợp đa thừa kế, điều này làm tăng thêm sự cố. Đây không phải là vấn đề với thừa kế duy nhất, mặc dù. –

+1

@Daniel: ngoại trừ nếu bạn sử dụng thừa kế 'ảo' ...trừ khi lớp cơ sở của bạn không có các phương thức ảo và lớp dẫn xuất có (trên hầu hết các triển khai); nó không được đảm bảo bởi tiêu chuẩn do đó nó là một lỗi trong chờ đợi. –

3

Các vấn đề chung với hiệp phương sai trong các thùng chứa như sau:

Hãy nói rằng dàn diễn viên của bạn sẽ làm việc và hợp pháp (nó không được nhưng chúng ta hãy giả định nó là dành cho ví dụ sau):

#include <vector> 
struct A {}; 
struct B : A { public: int Method(int x, int z); }; 
struct C : A { public: bool Method(char y); }; 
std::vector<B*> test; 
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test); 
foo->push_back(new C); 
test[0]->Method(7, 99); // What should happen here??? 

vì vậy, bạn cũng diễn giải-đúc một C * với một B * ...

Thực ra tôi không biết .NET và Java quản lý điều này như thế nào (tôi nghĩ rằng họ ném một ngoại lệ khi cố gắng chèn C).

+0

Điểm tốt. Mặc dù tôi đã nhận thức được thực tế rằng foo không được sửa đổi. Tôi nên tuyên bố nó là 'const'. –

+0

Java và C# ngăn chặn nó. Đó là những gì 'Danh sách 'about: bạn không thể gọi' x.add (someBaseObject) 'khi' x' có kiểu đó. – Norswap

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