sobota, 22 października 2011

Opakowywanie struktur języka C

W pierwszym wpisie na blogu chciałbym przedstawić problem, z którym zetknąłem się w pracy. Niezwykle często (a w większych projektach praktycznie zawsze) w tworzonej aplikacji niezbędne jest przesyłanie komunikatów pomiędzy obiektami. W moim przypadku były to wiadomości przesyłane pomiędzy modułami tworzonego sytemu - powiedzmy, że musiały one ze sobą "rozmawiać". Problemem było to, że wiadomości te spływały do nas "gdzieś z góry" i były to zwykłe struktury języka C - zero polimorfizmu czy interfejsów (mimo, że projekt tworzony był w języku C++). Co więcej dla nas były one jedynie read-only. Musieliśmy ich używać, ale nie mogliśmy ich "ulepszać".

Zaprezentuję sposób w jaki struktury języka C można opakować w klasy języka C++ bez edycji tych pierwszych.

Problem rozwiążemy na przykładzie mebli. Załóżmy, że musimy operować na następujących trzech strukturach:

struct Chair {
  static const char* const name;
  // dodatkowe informacje na temat Chair
};

struct Table {
  static const char* const name;
  // dodatkowe informacje na temat Table
};

struct Bed {
  static const char* const name;
  // dodatkowe informacje na temat Bed
};

Dla ułatwienia każdy mebel zawiera swoją nazwę. Przyda nam się to później, aby upewnić się, że nasze rozwiązanie działa :) Zdefiniujmy nazwy mebli następująco:

const char* const Chair::name = "chair";
const char* const Table::name = "table";
const char* const Bed::name = "bed";

Do naszych mebli dodamy najpierw polimorfizm. Nasze nowe meble będą dziedzyczyć po wspólnej klasie bazowej:

class FurnitureBase {
public:
  FurnitureBase(const std::size_t id_) : id(id_) {}
  virtual ~FurnitureBase() {}
  std::size_t getId() const { return id; }
  virtual const char* introduce() const = 0;

private:
    const std::size_t id;
};

Mając dany wskaźnik (lub referencję) do klasy bazowej, chcielibyśmy się dowiedzieć na jaki konkretnie mebel on wskazuje. Do tego celu przypiszmy każdemu meblowi unikalny numer ID. Dobrym rozwiązaniem jest zastosowanie tutaj tzw. klas cech charakterystycznych (ang. traits classes).

namespace FurnitureTraits {

template<class T>
struct id;

template<>
struct id<Chair> : boost::mpl::integral_c<std::size_t, 1> {};

template<>
struct id<Table> : boost::mpl::integral_c<std::size_t, 2> {};

template<>
struct id<Bed> : boost::mpl::integral_c<std::size_t, 3> {};

} // namespace FurnitureTraits

W naszym przykładzie krzesło ma numer ID równy 1, stół 2, a łóżko 3. Mamy już wszystko gotowe, aby stworzyć szablonową klasę dla naszych mebli:

template<class T>
class Furniture: public FurnitureBase {
public:
  Furniture(const T& furniture) : FurnitureBase(FurnitureTraits::id<T>::value) {
    this->furniture = furniture;
  }
  ~Furniture() {}
  T& getFurniture() { return furniture; }
  const char* introduce() const { return furniture.name; }

private:
  T furniture;
};

Opakowując nasze meble klasą Furniture nie tracimy żadnych informacji (wciąż mamy dostęp do surowych struktur języka C). Co więcej dodajemy do naszego projektu możliwości dynamicznego polimorfizmu (za pomocą funkcji wirtualnych) i tworzenie dowolnych interfejsów dla naszych mebli.

Poniżej krótka prezentacja tego co uzyskaliśmy:

template<class T>
void tryRawData(boost::shared_ptr<FurnitureBase> fb) {
  Furniture<T>& furniture = dynamic_cast< Furniture<T>& >(*fb);
  // dostep do surowych danych:
  T& rawFurniture = furniture.getFurniture();
  std::cout << rawFurniture.name << std::endl;
}

int main() {
  using namespace boost::assign;

  Chair chair;
  Table table;
  Bed bed;

  boost::shared_ptr< FurnitureBase > fChair(new Furniture<Chair>(chair));
  boost::shared_ptr< FurnitureBase > fTable(new Furniture<Table>(table));
  boost::shared_ptr< FurnitureBase > fBed(new Furniture<Bed>(bed));

  std::vector< boost::shared_ptr<FurnitureBase> > furniture;
  furniture += fChair;
  furniture += fTable;
  furniture += fBed;

  while(true) {
    std::size_t i;
    std::cin >> i;

    // polimorfizm:
    if(i < furniture.size()) {
      std::cout << "polymorphism: "<< std::endl;
      std::cout << furniture[i]->introduce() << std::endl;
    }

    // dostep do surowych danych:
    std::cout << "trying raw data: "<< std::endl;
    try {
      switch(i) {
        case FurnitureTraits::id<Chair>::value :
          tryRawData<Chair>(furniture.back());
          break;
        case FurnitureTraits::id<Table>::value :
          tryRawData<Table>(furniture.back());
          break;
        case FurnitureTraits::id<Bed>::value :
          tryRawData<Bed>(furniture.back());
          break;
        default:
          std::cout << "unknown id" << std::endl;
      }
    }
    catch(std::bad_cast&) {
      std::cerr << "bad cast" << std::endl;
    }
  }
}

Jak widać nic nie stoi nawet na przeszkodzie, żeby trzymać wszystkie meble w jednym kontenerze (w tym przypadku std::vector), co oczywiście byłoby niemożliwe dla wyjściowych mebli w postaci struktur języka C.

PS. Tak, wiem, że ten switch jest brzydki, ale może o dispatchingu w innym artykule. Tutaj miało być prosto i na temat ;)



Pliki do pobrania:

2 komentarze:

  1. Znalazłem drobny błąd w ostatnim akapicie "nic nie szkodzi nawet na przeszkodzie" ;)

    ps. Konieczność deklarowania typów zmiennych jest dla mnie jakaś taka nienaturalna. ;)
    ps2. Powodzenia w prowadzeniu bloga ;)

    OdpowiedzUsuń
  2. Dzieki, poprawione.

    PS. Kwestia przyzwyczajeń. Zarówno kontrola typów jak i jej brak mają swoje dobre i złe strony. ;)

    OdpowiedzUsuń