2015-05-12 24 views
5

W złożonym kodzie, to ma szereg niewirtualny klasy bazowej wskaźnik (klasa podstawa ma żadnych metod wirtualnych)C++ niebezpieczne obejście obsada

Rozważmy kod:

#include <iostream> 

using namespace std; 

class TBase 
{ 
    public: 
     TBase(int i = 0) : m_iData(i) {} 
     ~TBase(void) {} 

     void Print(void) {std::cout << "Data = " << m_iData << std::endl;} 

    protected: 
     int  m_iData; 
}; 

class TStaticDerived : public TBase 
{ 
    public: 
     TStaticDerived(void) : TBase(1) {} 
     ~TStaticDerived(void) {} 
}; 

class TVirtualDerived : public TBase 
{ 
    public: 
     TVirtualDerived(void) : TBase(2) {} 
     virtual ~TVirtualDerived(void) {} //will force the creation of a VTABLE 
}; 

void PrintType(TBase *pBase) 
{ 
    pBase->Print(); 
} 

void PrintType(void** pArray, size_t iSize) 
{ 
    for(size_t i = 0; i < iSize; i++) 
    { 
     TBase *pBase = (TBase*) pArray[i]; 
     pBase->Print(); 
    } 
} 


int main() 
{ 
    TBase b(0); 
    TStaticDerived sd; 
    TVirtualDerived vd; 

    PrintType(&b); 
    PrintType(&sd); 
    PrintType(&vd); //OK 

    void* vArray[3]; 
    vArray[0] = &b; 
    vArray[1] = &sd; 
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK 
    PrintType(vArray, 3); 

    return 0; 
} 

wyjście jest (przygotowane z Mingw-W64 GCC 4.9.2 na Win64)

Data = 0 
Data = 1 
Data = 2 
Data = 0 
Data = 1 
Data = 4771632 

przyczyną niepowodzenia jest to, że każde wystąpienie TVirtualDerived ma wskaźnik do tablicy wirtualnej, która nie ma TBase. Tak więc up-casting do TBase bez wcześniejszej informacji o typie (od void * do TBase *) nie jest bezpieczny.

Chodzi o to, że w pierwszej kolejności nie mogę uniknąć rzucania w pustkę. Dodanie metody wirtualne (destructor na przykład) na prace klasy bazowej, ale kosztem pamięci (które chcę uniknąć)

Kontekst:

wdrażamy system sygnał/gniazda, w bardzo ograniczone środowisko (pamięć poważnie ograniczona). Ponieważ mamy kilka milionów obiektów, które mogą wysyłać i odbierać sygnały, ten rodzaj optymalizacji jest skuteczne (jeśli to działa, oczywiście)

Pytanie:

Jak mogę rozwiązać ten problem? Do tej pory znalazłem:

1 - dodać wirtualną metodę w TBase. Działa, ale tak naprawdę nie rozwiązuje problemu, unika go. I jest to nieefektywne (za dużo pamięci).

2 - rzutowanie na TBase * zamiast rzucania do pustki * w układzie, kosztem utraty ogólności. (prawdopodobnie to, co spróbuję dalej)

Czy widzisz inne rozwiązanie?

+1

Czy po prostu rzutowanie na 'TBase *' * przed * rzutowaniem na 'void *' rozwiązuje problem do zadowolenia? ([Zobacz tutaj] (https : //ideone.com/kpNhe6)) –

+1

Po prostu dla jasności: Niektóre z twoich klas pochodnych mają metody wirtualne. Inni nie. A sama TBase jest tak mała, że ​​dodanie wskaźnika vtable powoduje znaczny wzrost wielkości pamięci. Poprawny? –

+0

Czy rozważałeś użycie szablonów? – cup

Odpowiedz

3

Musisz zastanowić się, w jaki sposób klasa jest umieszczona w pamięci. TBase jest łatwe, to tylko cztery bajty z jednego elementu:

_ _ _ _ 
|_|_|_|_| 
^ 
m_iData 

TStaticDerived jest taka sama. Jednak TVirtualDerived jest zupełnie inny. Ma teraz przyrównanie 8 i musi zacząć się przód z vtable, zawierający pozycje dla destructor:

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| 
^    ^
vtable   m_iData 

Więc kiedy rzucasz vd do void* a następnie TBase*, jesteś skutecznie reinterpretacji pierwsze cztery bajty twojego vtable (adres przesunięcia do ~TVirtualDerived()) jako m_iData. Rozwiązaniem jest najpierw zrobić static_cast do TBase*, która zwraca wskaźnik do skorygowania początkowy punkt TBase w vd i następnie do void*:

vArray[2] = static_cast<TBase*>(&vd); // now, pointer is OK 
+0

Dziękuję za odpowiedź. Zaktualizowałem pytanie, aby wyjaśnić, dlaczego potrzebujemy użyć tego systemu. static_cast nie zadziała w przypadku wielokrotnego dziedziczenia. – Seb

+0

@Seb Powróciłem do edycji, ponieważ jest to ściana tekstu, która nie zmienia znacząco ducha pytania. Również 'static_cast' zdecydowanie działa z dziedziczeniem wielokrotnym - problem jest prawdopodobnie taki, że rzutowanie z powrotem na" void * "jest niepoprawne. – Barry

4

Problem jest w rzucasz. Podczas korzystania z odlewu typu C przez pustą, jest to odpowiednik reinterpret_cast, który może być słaby przy podklasowaniu. W pierwszej części typ jest dostępny dla kompilatora, a twoje rzuty są równoważne static_cast.

Ale nie rozumiem, dlaczego mówisz, że nie można uniknąć przesyłania do unieważnienia * w pierwszej kolejności. Jako PrintType wewnętrznie konwertuje void * na TBase *, można równie dobrze przekazać TBase **. W tym przypadku będzie to działać dobrze:

void PrintType(TBase** pArray, size_t iSize) 
{ 
    for(size_t i = 0; i < iSize; i++) 
    { 
     TBase *pBase = pArray[i]; 
     pBase->Print(); 
    } 
} 
... 
    TBase* vArray[3]; 
    vArray[0] = &b; 
    vArray[1] = &sd; 
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK 
    PrintType(vArray, 3); 

Ewentualnie, jeśli chcesz użyć void ** tablicę, trzeba jawnie upewnić, że to, co można umieścić w nim są tylko TBase *a nie wskaźnik do podklasy:

void* vArray[3]; 
vArray[0] = &b; 
vArray[1] = static_cast<TBase *>(&sd); 
vArray[2] = static_cast<TBase *>(&vd); 
PrintType(vArray, 3); 

Ci zarówno sposób prawidłowo Wydajność:

Data = 0 
Data = 1 
Data = 2 
Data = 0 
Data = 1 
Data = 2 
+0

Podany kod służy do zilustrowania problemu (minimalny przykład). W naszym codebase (> 800 000 linii kodu) mamy klasę delegatów bez typu (zobacz ten artykuł dla głównej idei: [delegaci] (http://www.codeproject.com/Articles/7150/Member-Function-Pointers -i-the-Fastest-Possible)). Przesyłanie do TBase działa w większości przypadków, ale gdy jest używane wielokrotne dziedziczenie, to już nie działa - potrzebujemy najbardziej oryginalnego typu, lub kompilator nie będzie w stanie poprawnie wywnioskować adresu metody. Właśnie dlatego rzutujemy wskaźnik obiektu na puste - aby zachować oryginalny wskaźnik. – Seb

0

Zapomnij o wirtualnym polimorfizmie. Zrób to w staroświecki sposób.

Dodaj jeden bajt do każdego zwarcia, aby wskazać typ i instrukcję przełącznika w metodzie drukowania na "Zrób właściwą rzecz". (oszczędza to sizeof (wskaźnik) -1 bajtów na TBase w stosunku do metody wirtualnej:

Jeśli dodanie bajtu jest nadal zbyt kosztowne, rozważ użycie pól bitowych C/C++ (każdy pamięta te (uśmiech)), aby wycisnąć wpisz pole do innego pola, które nie wypełnia dostępnej przestrzeni (na przykład niepodpisana liczba całkowita, która ma maksymalną wartość 2^24 - 1)

Twój kod będzie brzydki, prawdziwy, ale twoje surowe ograniczenia pamięci są Brzydki kod działa lepiej niż piękny kod, który się nie powiódł

+0

Dziękuję za odpowiedź. Próbowaliśmy tego podejścia i działa ono dla większości klas. Ale mamy problem, gdy chcemy czerpać z TBase i innej klasy (z innej biblioteki) metodami wirtualnymi. Właśnie dlatego publikuję pytanie. Staramy się zrobić skuteczny kod i jest on już bardzo brzydki! (ale wydajne!). Problem z brzydkim to konserwacja: jest kosztowna (wymaga więcej czasu).A kiedy potrzebujemy rozszerzyć/załatać kod 6 miesięcy później, będziemy żałować. – Seb