2011-01-14 19 views
12

Mam starszego kodu, który zamiast funkcji wirtualnych używa pola kind do dynamicznego wysyłania. Wygląda to mniej więcej tak:Wysyłanie dynamiczne w języku C++ bez funkcji wirtualnych

// Base struct shared by all subtypes 
// Plain-old data; can't use virtual functions 
struct POD 
{ 
    int kind; 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 
    int GetXyzzy(); 
}; 

enum Kind { Kind_Derived1, Kind_Derived2, Kind_Derived3 /* , ... */ }; 

struct Derived1: POD 
{ 
    Derived1(): kind(Kind_Derived1) {} 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 
    int GetXyzzy(); 

    // ... plus other type-specific data and function members ... 
}; 

struct Derived2: POD 
{ 
    Derived2(): kind(Kind_Derived2) {} 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 
    int GetXyzzy(); 

    // ... plus other type-specific data and function members ... 
}; 

struct Derived3: POD 
{ 
    Derived3(): kind(Kind_Derived3) {} 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 
    int GetXyzzy(); 

    // ... plus other type-specific data and function members ... 
}; 

// ... and so on for other derived classes ... 

a następnie członkowie funkcyjne klasy za POD są realizowane tak:

int POD::GetFoo() 
{ 
    // Call kind-specific function 
    switch (kind) 
    { 
    case Kind_Derived1: 
     { 
     Derived1 *pDerived1 = static_cast<Derived1*>(this); 
     return pDerived1->GetFoo(); 
     } 
    case Kind_Derived2: 
     { 
     Derived2 *pDerived2 = static_cast<Derived2*>(this); 
     return pDerived2->GetFoo(); 
     } 
    case Kind_Derived3: 
     { 
     Derived3 *pDerived3 = static_cast<Derived3*>(this); 
     return pDerived3->GetFoo(); 
     } 

    // ... and so on for other derived classes ... 

    default: 
     throw UnknownKindException(kind, "GetFoo"); 
    } 
} 

POD::GetBar(), POD::GetBaz(), POD::GetXyzzy() i inni członkowie są realizowane w podobny sposób.

Ten przykład jest uproszczony. Rzeczywisty kod ma około tuzina różnych podtypów POD i kilkudziesięciu metod. Nowe podtypy POD i nowe metody dodawane są dość często, więc za każdym razem, gdy to robimy, musimy zaktualizować wszystkie te instrukcje.

Typowym sposobem na obsłużenie tego byłoby zadeklarowanie elementów funkcji virtual w klasie , ale nie możemy tego zrobić, ponieważ obiekty znajdują się we wspólnej pamięci. Jest dużo kodu, który zależy od tego, czy te struktury są zwykłymi danymi, więc nawet gdybym mógł znaleźć jakiś sposób na posiadanie wirtualnych funkcji w obiektach pamięci wspólnej, nie chciałbym tego robić.

Poszukuję więc sugestii, jak najlepiej to oczyścić, aby cała wiedza na temat metod wywoływania podtypów była scentralizowana w jednym miejscu, a nie rozproszona pośród kilkudziesięciu switch kilkadziesiąt funkcji.

To, co mi się przydarza, to że mogę stworzyć jakąś klasę adaptera, która owija POD i używa szablonów w celu zminimalizowania nadmiarowości. Zanim jednak rozpocznę tę drogę, chciałbym się dowiedzieć, jak inni sobie z tym poradzili.

+0

Mówiłeś było dużo kodu w zależności od tej klasy. Czy możesz dodać do niego pola lub czy struktura musi pozostać taka sama? –

+0

Struktura powinna pozostać zasadniczo taka sama. Posiadamy masę ogromnych tablic takich rzeczy we wspólnej pamięci i już przekraczamy granice wielkości pamięci. –

+0

czy wszystkie procesy mają tę samą wersję biblioteki, czy nie? –

Odpowiedz

11

Możesz użyć tabeli skoku. Oto jak wygląda większość wirtualnych wysyłek pod maską i możesz ją skonstruować ręcznie.

template<typename T> int get_derived_foo(POD*ptr) { 
    return static_cast<T>(ptr)->GetFoo(); 
} 
int (*)(POD*) funcs[] = { 
    get_derived_foo<Derived1>, 
    get_derived_foo<Derived2>, 
    get_derived_foo<Derived3> 
}; 
int POD::GetFoo() { 
    return funcs[kind](this); 
} 

Krótki przykład.

Jakie są dokładnie ograniczenia związane z przebywaniem w pamięci współdzielonej? Zdałem sobie sprawę, że nie wiem wystarczająco dużo tutaj. Czy to znaczy, że nie mogę używać wskaźników, ponieważ ktoś w innym procesie będzie próbował użyć tych wskaźników?

Można użyć mapy strun, w której każdy proces otrzymuje własną kopię mapy. Musiałbyś przekazać to do GetFoo(), aby mógł go znaleźć.

struct POD { 
    int GetFoo(std::map<int, std::function<int()>& ref) { 
     return ref[kind](); 
    } 
}; 

Edycja: Oczywiście, nie musisz używać ciąg tutaj, możesz użyć int. Po prostu użyłem tego jako przykładu. Powinienem to zmienić. W rzeczywistości rozwiązanie to jest dość elastyczne, ale ważne jest, aby utworzyć kopię danych specyficznych dla procesu, np. Wskaźniki funkcji lub cokolwiek innego, a następnie przekazać je.

+0

Obiekty w pamięci współdzielonej są używane przez wiele procesów. Wskaźnik funkcji wirtualnej nie będzie poprawny dla wszystkich procesów. –

+2

Nie zajrzałem do szczegółów, ale ogólnie rzecz biorąc, do tabeli odwołuje się wskaźnik w każdym obiekcie. Jeśli wątek ma vtable dla typu X pod adresem Y, zapisze Y w polu vptr obiektu.Nie gwarantuje, że nawet jeśli tabela vt będzie przechowywana w dokładnie tym samym adresie sprzętowym w systemie, dwa różne procesy nie zobaczą jej w różnych adresach logicznych. W takim przypadku inny wątek spróbuje użyć tabeli vtable pod niewłaściwym adresem i umrze. –

+1

Fajnie, ale byłoby wspaniale, gdybyś poprawnie wygenerował tablice. Makro preprocesora może to zrobić. –

0

Oto przykład użycia Ciekawie powtarzającego się wzorca szablonu. Może to odpowiadać twoim potrzebom, jeśli wiesz więcej informacji w czasie kompilacji.

template<class DerivedType> 
struct POD 
{ 
    int GetFoo() 
    { 
     return static_cast<DerivedType*>(this)->GetFoo(); 
    } 
    int GetBar() 
    { 
     return static_cast<DerivedType*>(this).GetBar(); 
    } 
    int GetBaz() 
    { 
     return static_cast<DerivedType*>(this).GetBaz(); 
    } 
    int GetXyzzy() 
    { 
     return static_cast<DerivedType*>(this).GetXyzzy(); 
    } 
}; 

struct Derived1 : public POD<Derived1> 
{ 
    int GetFoo() 
    { 
     return 1; 
    } 
    //define all implementations 
}; 

struct Derived2 : public POD<Derived2> 
{ 
    //define all implementations 

}; 

int main() 
{ 
    Derived1 d1; 
    cout << d1.GetFoo() << endl; 
    POD<Derived1> *p = new Derived1; 
    cout << p->GetFoo() << endl; 
    return 0; 
} 
+0

Ale co mam zrobić, jeśli zacznę od 'POD *', które faktycznie wskazuje na podtyp i chcę wywołać 'GetFoo()'? –

1

Oto podejście, które wykorzystuje wirtualne metody do implementacji tabeli przeskoku, bez potrzeby, aby klasa Pod lub klasy pochodne rzeczywiście miały funkcje wirtualne.

Celem jest uproszczenie dodawania i usuwania metod w wielu klasach.

Aby dodać metodę, należy ją dodać do Pod za pomocą jasnego i wspólnego wzorca, do PodInterface musi zostać dodana czysta funkcja wirtualna, a funkcja Podsyłania musi zostać dodana do PodFuncs za pomocą wyraźnego i wspólnego wzorca.

Klasy pochodne wymagają tylko statycznego obiektu inicjalizacyjnego pliku do ustawienia rzeczy, w przeciwnym razie wyglądają tak, jak już to robią.

// Pod header 

#include <boost/shared_ptr.hpp> 
enum Kind { Kind_Derived1, Kind_Derived2, Kind_Derived3 /* , ... */ }; 

struct Pod 
{ 
    int kind; 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 
}; 

struct PodInterface 
{ 
    virtual ~PodInterface(); 

    virtual int GetFoo(Pod* p) const = 0; 
    virtual int GetBar(Pod* p) const = 0; 
    virtual int GetBaz(Pod* p) const = 0; 

    static void 
    do_init(
      boost::shared_ptr<PodInterface const> const& p, 
      int kind); 
}; 

template<class T> struct PodFuncs : public PodInterface 
{ 
    struct Init 
    { 
     Init(int kind) 
     { 
      boost::shared_ptr<PodInterface> t(new PodFuncs); 
      PodInterface::do_init(t, kind); 
     } 
    }; 

    ~PodFuncs() { } 

    int GetFoo(Pod* p) const { return static_cast<T*>(p)->GetFoo(); } 
    int GetBar(Pod* p) const { return static_cast<T*>(p)->GetBar(); } 
    int GetBaz(Pod* p) const { return static_cast<T*>(p)->GetBaz(); } 
}; 


// 
// Pod Implementation 
// 

#include <map> 

typedef std::map<int, boost::shared_ptr<PodInterface const> > FuncMap; 

static FuncMap& get_funcmap() 
{ 
    // Replace with other approach for static initialisation order as appropriate. 
    static FuncMap s_funcmap; 
    return s_funcmap; 
} 

// 
// struct Pod methods 
// 

int Pod::GetFoo() 
{ 
    return get_funcmap()[kind]->GetFoo(this); 
} 

// 
// struct PodInterface methods, in same file as s_funcs 
// 

PodInterface::~PodInterface() 
{ 
} 

void 
PodInterface::do_init(
     boost::shared_ptr<PodInterface const> const& p, 
     int kind) 
{ 
    // Could do checking for duplicates here. 
    get_funcmap()[kind] = p; 
} 

// 
// Derived1 
// 

struct Derived1 : Pod 
{ 
    Derived1() { kind = Kind_Derived1; } 

    int GetFoo(); 
    int GetBar(); 
    int GetBaz(); 

    // Whatever else. 
}; 

// 
// Derived1 implementation 
// 

static const PodFuncs<Derived1>::Init s_interface_init(Kind_Derived1); 

int Derived1::GetFoo() { /* Implement */ } 
int Derived1::GetBar() { /* Implement */ } 
int Derived1::GetBaz() { /* Implement */ } 
1

Oto ścieżka szablonu do metaprogramowania, którą teraz przechodzę. Oto, co lubię o tym:

  • Dodanie wsparcia dla nowego rodzaju wymaga jedynie aktualizacji LAST_KIND i dodanie nowego KindTraits.
  • Istnieje prosty wzorzec dodawania nowej funkcji.
  • W razie potrzeby funkcje mogą być wyspecjalizowane dla poszczególnych rodzajów.
  • Mogę spodziewać się błędów i ostrzeżeń podczas kompilacji, a nie tajemniczego zachowania w czasie pracy, jeśli coś zepsuję.

Istnieje kilka problemów:

  • POD „s implementacja jest teraz uzależnione interfejsów wszystkich klas pochodnych. (Jest to już prawdą w istniejącej implementacji, więc nie martwię się o to, ale to jest trochę powonieniem.)
  • Liczę na to, że kompilator będzie wystarczająco inteligentny, aby wygenerować kod, który jest mniej więcej równoważny do kodu opartego na switch.
  • Wielu programistów w C++ drapie się po obejrzeniu tego.

Oto kod:

// Declare first and last kinds 
const int FIRST_KIND = Kind_Derived1; 
const int LAST_KIND = Kind_Derived3; 

// Provide a compile-time mapping from a kind code to a subtype 
template <int KIND> 
struct KindTraits 
{ 
    typedef void Subtype; 
}; 
template <> KindTraits<Kind_Derived1> { typedef Derived1 Subtype; }; 
template <> KindTraits<Kind_Derived2> { typedef Derived2 Subtype; }; 
template <> KindTraits<Kind_Derived3> { typedef Derived3 Subtype; }; 

// If kind matches, then do the appropriate typecast and return result; 
// otherwise, try the next kind. 
template <int KIND> 
int GetFooForKind(POD *pod) 
{ 
    if (pod->kind == KIND) 
     return static_cast<KindTraits<KIND>::Subtype>(pod)->GetFoo(); 
    else 
     return GetFooForKind<KIND + 1>(); // try the next kind 
} 

// Specialization for LAST_KIND+1 
template <> int GetFooForKind<LAST_KIND + 1>(POD *pod) 
{ 
    // kind didn't match anything in FIRST_KIND..LAST_KIND 
    throw UnknownKindException(kind, "GetFoo"); 
} 

// Now POD's function members can be implemented like this: 

int POD::GetFoo() 
{ 
    return GetFooForKind<FIRST_KIND>(this); 
} 
+1

Tęskniłeś za szansą rzucenia wyjątku UnkindException! To podejście wymaga, aby implementacja POD zobaczyła definicję każdego wyprowadzonego typu. Biorąc pod uwagę obecny stan kodu, możesz nie dbać o to, ale wymagania są spełnione. – janm

+0

Dodałem notatkę o zależności do mojej sekcji "Obawy". –

0

Rozszerzając roztworze zakończyło się z następującą rozwiązuje mapowania do funkcji pochodzących przy inicjalizacji programu:

#include <typeinfo> 
#include <iostream> 
#include <functional> 
#include <vector> 

enum Kind 
{ 
    Kind_First, 
    Kind_Derived1 = Kind_First, 
    Kind_Derived2, 
    Kind_Total 
}; 

struct POD 
{ 
    size_t kind; 

    int GetFoo(); 
    int GetBar(); 
}; 

struct VTable 
{ 
    std::function<int(POD*)> GetFoo; 
    std::function<int(POD*)> GetBar; 
}; 

template<int KIND> 
struct KindTraits 
{ 
    typedef POD KindType; 
}; 

template<int KIND> 
void InitRegistry(std::vector<VTable> &t) 
{ 
    typedef typename KindTraits<KIND>::KindType KindType; 

    size_t i = KIND; 
    t[i].GetFoo = [](POD *p) -> int { 
     return static_cast<KindType*>(p)->GetFoo(); 
    }; 
    t[i].GetBar = [](POD *p) -> int { 
     return static_cast<KindType*>(p)->GetBar(); 
    }; 

    InitRegistry<KIND+1>(t); 
} 
template<> 
void InitRegistry<Kind_Total>(std::vector<VTable> &t) 
{ 
} 

struct Registry 
{ 
    std::vector<VTable> table; 

    Registry() 
    { 
     table.resize(Kind_Total); 
     InitRegistry<Kind_First>(table); 
    } 
}; 

Registry reg; 

int POD::GetFoo() { return reg.table[kind].GetFoo(this); } 
int POD::GetBar() { return reg.table[kind].GetBar(this); } 

struct Derived1 : POD 
{ 
    Derived1() { kind = Kind_Derived1; } 

    int GetFoo() { return 0; } 
    int GetBar() { return 1; } 
}; 
template<> struct KindTraits<Kind_Derived1> { typedef Derived1 KindType; }; 

struct Derived2 : POD 
{ 
    Derived2() { kind = Kind_Derived2; } 

    int GetFoo() { return 2; } 
    int GetBar() { return 3; } 
}; 
template<> struct KindTraits<Kind_Derived2> { typedef Derived2 KindType; }; 

int main() 
{ 
    Derived1 d1; 
    Derived2 d2; 
    POD *p; 

    p = static_cast<POD*>(&d1); 
    std::cout << p->GetFoo() << '\n'; 
    p = static_cast<POD*>(&d2); 
    std::cout << p->GetBar() << '\n'; 
} 
Powiązane problemy