2011-01-26 16 views
16

Wiem, że C++ nie obsługuje kowariancji dla elementów kontenerów, tak jak w Javie lub C#. Więc następujący kod prawdopodobnie jest niezdefiniowane zachowanie:Kontener kowariancji w C++

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

Nic więc dziwnego, że otrzymała downvotes gdy sugeruje to rozwiązanie another question.

Ale jaka część standardu C++ dokładnie mówi mi, że spowoduje to niezdefiniowane zachowanie? Gwarantuje to, że zarówno wskaźniki std::vector<A*> jak i std::vector<B*> przechowują swoje wskaźniki w ciągłym bloku pamięci. Gwarantuje to również, że sizeof(A*) == sizeof(B*). Wreszcie, A* a = new B jest całkowicie legalny.

Więc jakie złe duchy w standardzie wyczarowałem (poza stylem)?

+1

Użycie reinterpret_cast <>() po tym punkcie nic nie jest zdefiniowane. To może zadziałać, ale twoja lista warunków jest okropnie krótka. Dodałbym jeszcze kilka wstępnych warunków. sizeof (A) == sizeof (B); Ani A, ani B nie mogą zawierać żadnych typów funkcji wirtualnych. Ani A, ani B, ani żaden potomek umieszczony w tablicy nie może używać dziedziczenia wielokrotnego. –

+7

Odpowiedź na niecertyfikowaną C++ polega na tym, że nie jest ona bezpieczna dla typów.Jeśli dodasz 'A' do foo, masz test w stanie niepoprawnym, ponieważ gwarantuje to, że wszystkie elementy są typu' B'. A C# też tego nie obsługuje. C# obsługuje tylko dla ogólnych parametrów, które są używane w bezpieczny sposób (tylko wejściowe lub wyjściowe) i tylko w interfejsach i delegatach. Java obsługuje go, ponieważ dodaje kontrole czasu wykonywania i wewnętrznie działa na obiekcie klasy podstawowej. – CodesInChaos

+0

To pytanie wygląda podobnie do http://stackoverflow.com/questions/842387/how-do-i-dynamically-cast-between-vectors-ofpointpoints – Nekuromento

Odpowiedz

16

Reguła naruszone tutaj jest udokumentowana w C++ 03 3,10/15 [basic.lval], która określa, co jest określane jako „nieformalnie ścisłe reguły aliasing”

Jeśli program próbuje uzyskać dostęp do zapisana wartość przedmiotu poprzez lwartością z innych niż jeden z następujących typów zachowanie jest niezdefiniowane:

  • dynamiczny rodzaj obiektu,

  • wersja cv wykwalifikowany dynamicznego typu obiektu,

  • typ, który jest podpisany lub typ unsigned odpowiadający dynamicznego typu obiektu,

  • typ, który jest podpisany lub niepodpisane typ odpowiadający CV- wykwalifikowany wersja dynamicznego typu obiektu,

  • agregatem lub unia typ, który zawiera jeden z wyżej wymienionych typów wśród swoich członków (w tym rekurencyjnie, członek subaggregate lub zawartej unii),

  • typ, który jest (możliwe bly cv-qualified) typ klasy podstawowej dynamicznego typu obiektu,

  • char lub unsigned char type.

Krótko mówiąc, biorąc pod uwagę przedmiot, są dozwolone tylko w celu uzyskania dostępu do tego obiektu za pomocą wyrażenia, które ma jeden z typów na liście. W przypadku obiektu typu klasowego, który nie ma klas podstawowych, takich jak std::vector<T>, w zasadzie ogranicza się do typów wymienionych w punktach pierwszym, drugim i ostatnim.

std::vector<Base*> i std::vector<Derived*> są zupełnie niepowiązanych typy i nie można korzystać z obiektu typu std::vector<Base*> jakby to była std::vector<Derived*>.Kompilator może zrobić różne rzeczy, jeśli narusza tę zasadę, w tym:

  • wykonywać różne optymalizacje na jednym niż z drugiej, albo

  • rozplanować wewnętrzne członków jednej inaczej lub

  • wykonywać optymalizacje zakładając, że std::vector<Base*>* nie może odnosić się do tego samego obiektu jako std::vector<Derived*>*

  • kontroli użycie wykonawczych zapewniających t Kapelusz nie łamią ścisłego aliasingu regułę

[Może to również zrobić żadnej z tych rzeczy, a to może „pracować”, ale nie ma gwarancji, że będzie to „praca” i jeśli zmienisz kompilatory lub kompilator wersje lub ustawienia kompilacji, może przestać działać. Używam tutaj przerażających cytatów z jakiegoś powodu. :-)]

Nawet jeśli po prostu miałeś Base*[N], nie możesz użyć tej tablicy tak, jakby była to Derived*[N] (chociaż w tym przypadku użycie byłoby bezpieczniejsze, gdzie "bezpieczniejsze" oznacza "wciąż niezdefiniowane, ale mniej prawdopodobne, aby dostać się w kłopoty).

+0

@ James: dzięki za przybycie ;-) Wow, twój ostatni komentarz jest dla mnie prawdziwą niespodzianką. Nie uważam się za początkującego w C++ po 15 latach pracy z nim, ale nigdy bym nie pomyślał, że to (nieprawidłowe zachowanie) ma zastosowanie nawet w przypadku tablic! Dzięki. –

+0

To ('Base [N]' vs 'Derived [N]') wpędzi cię zdecydowanie w kłopoty, jeśli rozmiary są różne, np. jeśli klasa pochodna ma więcej członków. – etarion

+0

@etarion: mówimy tutaj o wskaźnikach: np. 'F (Base a []) {...}' i przekazanie go (poprzez 'reinterpret_cast') tablicy' Derived'. –

4

jesteś powołując się na zły duch reinterpret_cast <>.

chyba że naprawdę wiesz co robisz (to znaczy nie dumnie i nie pedantycznie) reinterpret_cast jest jedną z bram zło:

Jedyne bezpieczne użycie, jakie znam, to zarządzanie klasą es i struktury między wywołaniami funkcji C++ i C. Może jednak są inne.

+1

Innym sensownym zastosowaniem jest przybliżenie "szybkiej matematyki" wykorzystujące reprezentację liczb zmiennoprzecinkowych. Ryzyko jest zasadniczo eliminowane przez wymagania przybliżenia, np. [fast inv sqrt] (https://en.wikipedia.org/wiki/Fast_inverse_square_root), który (nie tylko) działa wyłącznie na '32-bitowych liczbach zmiennoprzecinkowych [s] w formacie IEEE 754 zmiennoprzecinkowy'. TL; DR steruj czysto, chyba że jesteś profesjonalnym "demonem nosa" (https://en.wikipedia.org/wiki/Undefined_behavior) "wrangler. –

2

Myślę, że łatwiej będzie pokazać, niż powiedzieć:

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; 
} 

The (specyficzne) Emisja pokazał tutaj jest to, że kiedy robi „właściwą” konwersja, kompilator dodaje jakiś wskaźnik ajdustement w zależności od pamięci układ obiektów. Na reinterpret_cast nie jest przeprowadzana regulacja.

Przypuszczam, zrozumiesz dlaczego stosowanie reinterpet_cast powinny normalnie zostać wyrzucony z kodem ...

+0

Tak, w przypadku dziedziczenia wielokrotnego, zwiększa to kłopot. Nie dotyczy to jednak pojedynczego dziedziczenia. –

+1

@Daniel: z wyjątkiem sytuacji, gdy używasz dziedziczenia "wirtualnego" ...chyba że twoja klasa podstawowa nie ma metod wirtualnych, a klasa pochodna ma (w większości implementacji); nie jest gwarantowane przez standard, więc jest to błąd w oczekiwaniu. –

3

Ogólny problem z kowariancji w pojemnikach jest następujący:

Powiedzmy swoją obsadę będzie działać i być legalne (nie jest ale załóżmy, że jest na poniższym przykładzie):

#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??? 

więc trzeba także reinterpretacji-lanego C * do B * ...

Właściwie nie wiem jak to zrobić .NET i Java (myślę, że rzucają wyjątek podczas próby wstawienia C).

+0

Dobra uwaga. Chociaż zdawałem sobie sprawę z faktu, że foo nie może być modyfikowany. Powinienem był zadeklarować to jako "const". –

+0

Java i C# zapobiegają temu. Tak właśnie jest 'List 'is about: nie można wywołać' x.add (someBaseObject) ', gdy' x' ma ten typ. – Norswap

Powiązane problemy