Pytanie Polimorfizm C ++ obiektu w tablicy


Jestem inżynierem oprogramowania wbudowanym i pochodzę ze świata bitów i C. W tym świecie są dane w pamięci flash reprezentowane przez const w C. I są dane w pamięci RAM. RAM-y są drogie i ograniczone, a pamięć flash jest tania i wystarczająca. Ponadto, dynamiczne przydzielanie pamięci przy użyciu nowych, usuwanie, malloc itp. Jest niedozwolone ze względu na problem fragmentacji lub przepisy bezpieczeństwa, preferowane są projekty statyczne.

Mam około 2000 obiektów, które mają podobne stałe właściwości, ale różne zachowania. Tak więc dla nich zdefiniowałem klasę kształtu jako klasę bazową, która przechowuje współużytkowane właściwości moich obiektów. Aby reprezentować różne zachowania, klasa Shape ma jedną abstrakcyjną metodę o nazwie Print (), która zostanie nadpisana przez rodziców.

ShapeList jest ważną częścią. Jest to tablica stałych składających się z "stałych kształtów", które zostaną umieszczone w sekcji pamięci flash za pomocą łącznika.

Poniżej program generuje wynik:

I'm a Shape has 3 dots
I'm a Shape has 4 dots
I'm a Shape has 5 dots

Oczekiwane wyniki to:

I'm a Triangle has 3 dots
I'm a Rectangle has 4 dots
I'm a Pentagon has 5 dots

Potrzebuję zachowania polimorficznego. Kiedy drukuję Trójkąt, powinien zachowywać się jak Trójkąt, a nie Kształt. Jak mogę to zrobić?

Dzięki.

#include <array>
#include <cstdio>
class Shape
{
    public:
    const int DotCount;
    Shape(const int dot): DotCount(dot) {}
    virtual void Print(void) const; // this is virtual method
};

void Shape::Print(void) const
{
    printf("I'm a Shape has %d dots\n", DotCount);
}

class Triangle: public Shape
{
    public:
    Triangle(void): Shape(3) { }
    void Print(void) const;
};

void Triangle::Print(void) const
{
    printf("I'm a Triangle has %d dots\n", DotCount);
}

class Rectangle: public Shape
{
    public:
    Rectangle(void): Shape(4) { }
    void Print(void) const;
};

void Rectangle::Print(void) const
{
    printf("I'm a Rectangle has %d dots\n", DotCount);
}

class Pentagon: public Shape
{
    public:
    Pentagon(void): Shape(5) { }
    void Print(void) const;
};

void Pentagon::Print(void) const
{
    printf("I'm a Pentagon has %d dots\n", DotCount);
}

const std::array<const Shape, 3> ShapeList = { Triangle(), Rectangle(), Pentagon() };

int main(void)
{
    ShapeList.at(0).Print();
    ShapeList.at(1).Print();
    ShapeList.at(2).Print();
    return(0);
}

Więcej problemu: Dzisiaj zdałem sobie sprawę, że jest inny problem z funkcjami wirtualnymi. Kiedy dodaję jakiekolwiek wirtualne funkcje do klasy bazowej, kompilator zacznie ignorować dyrektywę "const" i automatycznie umieszcza obiekt w pamięci RAM zamiast w pamięci flash. Nie wiem dlaczego. Zadałem to pytanie IAR. Wniosek jaki do tej pory wyciągnąłem jest taki, że zachowanie polimorficzne nie jest możliwe przy klasach ROM, nawet z kupą: /


12
2017-12-24 17:08


pochodzenie


Muszą to być wskaźniki, w przeciwnym razie dostaniesz cięcie obiektów en.wikipedia.org/wiki/Object_slicing - gvd
możliwy duplikat Tablica polimorficznych obiektów klasy bazowej zainicjalizowanych obiektami klasy potomnej - juanchopanza
Nie czytałem dokładnie tego pytania. Also, dynamic memory allocation using new, delete, malloc etc is not allowed due to fragmentation problem or safety regulations, static designs are preferred. Z tego, co przeczytałem, nie można uzyskać polimorfizmu bez wskazówek i odnośników.
@juanchopanza: Nie ma rozwiązania mojego pytania. - Mehmet Fide
Pamiętaj, że niekoniecznie potrzebujesz new lub malloc. Po prostu trzeba przechowywać wskaźniki w tablicy. To, czy obiekty, do których wskazują, są przydzielane dynamicznie czy nie, jest kwestią prostopadłą. - juanchopanza


Odpowiedzi:


Jak zauważyli inni, wygodny i powszechny sposób nie działa w ten sposób. Naprawienie tego skutkuje kodem, który jest sprzeczny z ograniczeniami twojej platformy docelowej. Można jednak na różne sposoby emulować polimorfizm.

Możesz segregować obiekty według typu:

const Triangle tris[] = {tri1, tri2, ...};
const Rectangle rects[] = {rect1, rect2, ...};
// for correct order, if needed
const Shape * const shapes[] = {&tris[0], &rects[2], &rects[0], ...}:

Nadal musisz wykonać wszystkie metody (które zachowują się inaczej dla różnych typów) virtuali płacisz dodatkowy wskaźnik (dwa, jeśli policzysz wskaźnik vtable, który byłby trochę nieuczciwy) na obiekt. Możesz także usunąć wszystko virtual na rzecz jawnego tagu:

enum ShapeKind { Triangle, Rectangle, Pentagon };
struct Shape {
    ShapeKind kind;
    int sides;
    ...
};

Użyć union jeśli różne podklasy potrzebują bardzo różnych danych członków. Ma to wiele surowych ograniczeń i prowadzi do dość paskudnego kodu, ale może działać dobrze. Na przykład musisz znać swoją hierarchię z góry, a podklasy muszą być mniej więcej tej samej wielkości. Zauważ, że nie jest to koniecznie szybsze niż virtual alternatywa, ale jeśli ma zastosowanie, może zająć mniej miejsca (bajt zamiast wskaźnika vtable) i uczynić introspekcję szczuplejszą.


9
2017-12-24 17:29



"Zauważ, że to niekoniecznie jest szybsze" - prawda, ale kiedy to możliwe dużo szybciej dla trywialnych funkcji, które są wbudowane (do około rzędu wielkości w zależności od kompilatora / procesora itp.) ... - Tony Delroy
C ++ 17 std::variant daje ci bezpieczne połączenie. Możesz nawet użyć std::variant<Shape, Triangle, Rectangle> I użyć std::visit (przychodzi z variant) do wysyłki na podstawie odpowiedniego typu obiektu. To powinno dać ci o tej samej funkcjonalności (i wydajności runtime) jak enum ShapeKind+ ręczne wysyłanie. - Peter Cordes


Ta wersja nie korzysta z pamięci dynamicznej:

Triangle tri;
Rectangle rect;
Pentagon pent;
const std::array<const Shape*, 3> ShapeList {
    &tri, &rect, &pent
};
for (unsigned int i = 0; i < ShapeList.size(); i++)
    ShapeList[i]->Print();

W językach takich jak C # możesz użyć skrótu as słowo kluczowe w celu osiągnięcia "polimorfizmu". W C ++ wygląda to tak:

    const Triangle* tri = dynamic_cast<const Triangle*>(ShapeList[i]);
    if (tri)
        static_cast<Triangle>(*tri).SomethingSpecial();

Jeśli wskaźnik zwrócił wartość dynamic_cast jest ważny, możesz zadzwonić Trianglejest specjalna funkcja. To na przykład pozwoli ci mieć pętlę, która jest iterowana ShapeList i zadzwoń tylko Trianglemetody. Jeśli możesz używać wyjątków, pomyśl o zawinięciu go w try  catch blokować i łapać std::bad_cast.

Uwaga: potrzebujesz const wskaźnik, ponieważ ShapeList[i] jest const. Powód static_cast jest konieczne, ponieważ wywołujesz metodę nie const na wskaźniku const. Możesz dodać kwalifikator const, np SomethingSpecial() const i po prostu róbcie tri->SomethingSpecial(). W przeciwnym razie po prostu rzucisz const poza.

Na przykład:

static_cast<Triangle*>(tri)->SomethingSpecial();
// error: static_cast from type 'const Triangle*' to type 'Triangle*' 
// casts away qualifiers

To zadziała:

const_cast<Triangle*>(tri)->SomethingSpecial();

10
2017-12-24 17:11



@ChristianSchack Jakie byłyby korzyści? Zwłaszcza, gdy wszystkie podklasy wymagają różnych dodatkowych danych.
Himm te rozwiązania wyglądają mi dobrze, przynajmniej lepiej niż inne rozwiązania. Czy mogę zapytać, dlaczego to się zawiesza: "const std :: array <const Shape *, 3> ShapeList = {i Triangle (), & Rectangle () i Pentagon ()};" ? - Mehmet Fide
op powiedział, że dane są takie same dla wszystkich podklas. Więc argumenty funkcji nie zmienią się. tylko zachowanie. - Diversity
@Mehmet, który zawiesza się, ponieważ w tym przypadku bierzesz adres ukrytego tymczasowego. Zmienna tymczasowa wykracza poza zakres, pozostawiając zachowanie nieokreślone. Prawdopodobnie temp został przydzielony na stosie, który zostanie nadpisany później podczas wykonywania. - Mike Woolf
Wystarczy zauważyć, że static_cast <Triangle *> jest niepotrzebny, ponieważ tri jest już typu Triangle *. - Mike Woolf


Możesz użyć polimorfizmu wraz ze wszystkimi ograniczeniami, wprowadzając niewielką zmianę w kodzie:

const Triangle triangle;
const Rectangle rectangle;
const Pentagon pentagon;

const std::array<const Shape*, 3> ShapeList = { &triangle, &rectangle, &pentagon };

8
2017-12-24 17:41





Każda łatwa poprawka polegałaby na dodaniu ciągu znaków do kształtu, który definiuje jego kształt.

class Shape
{
    public:
    const int DotCount;
    const char* shapeType
    Shape(const int dot, const char* type): DotCount(dot), shapeType(type) {}
    void Print(void) const;
};

void Shape::Print(void) const
{
    printf("I'm a "); printf(shapeType); printf(" has %d dots\n", DotCount);
}

class Triangle: public Shape
{
    public:
    Triangle(void): Shape(3, "Triangle") { }
};

5
2017-12-24 17:29



dobra myśl. lepiej zapisać pole nazwy w klasie bazowej, ponieważ wszyscy go używają. - Itzik984
to nie jest dla mnie dobre. Funkcja Shape :: Print będzie rosnąć jak szalona, ​​aby wspierać wszystkie zachowania rodziców. Również wszyscy członkowie zespołu muszą zmodyfikować funkcję Shape :: Print () za każdym razem, gdy definiują nowy kształt, który ma inne zachowanie. - Mehmet Fide
cplusplus.com/doc/tutorial/polymorphism Cóż, twój kod wygląda na to, że powinien działać, przejrzyj ten link. Niestety moja odpowiedź nie pomogła w twojej sytuacji. - Jesse Laning
@MehmetFide dlaczego miałoby to zwariować? mimo to będzie używać wirtualnej tabeli, aby ostatecznie uzyskać nazwę, więc nie jest to ta sama podstawa? - Itzik984
@ Itzik984: Tak, ale wirtualna tabela będzie podlegać kompilatorowi. W proponowanym tutaj rozwiązaniu metoda Shape :: Print najprawdopodobniej musi zawierać ogromną zmianę, aby implementować zachowanie wszystkich rodziców w zależności od metody shapeType. Nie jest to również dobre podejście, jeśli jesteś zespołem i pracuje na nim wielu programistów. Wyobraź sobie, że zamierzasz zaimplementować Trójkąt, zrobię prostokąt, a ktoś inny zrobi więcej. Każdy z nas musi się zgodzić na shapeType i każdy musi rozszerzyć metodę Shape :: Print, aby wspierać swoje zachowanie kształtu. Dzięki prawdziwemu polimorficznemu rozwiązaniu każdy będzie niezależny. - Mehmet Fide


Innym rozwiązaniem, które znalazłem w C dla polimorficznej dynamicznej wysyłki bez alokacji dynamicznej, jest ręczne przekazywanie wskazówek do vtable, takich jak usuwanie przez GHC czcionek w Haskell. Takie podejście byłoby również sensowne w C ++, ponieważ jest lekkie i ściśle bardziej ogólne niż pozwala na to system obiektowy C ++.

Przeciążona / polimorficzna funkcja przyjmuje wskaźnik do struktury wskaźników funkcji dla każdej typografii, do której należy typ parametru - porównanie równości, porządkowanie, & c. Więc możesz mieć:

template<class Container, class Element>
struct Index {
  size_t (*size)(const Container& self);
  const Element& (*at)(const Container& self, size_t index);
};

enum Ordering { LT, EQ, GT };

template<class T>
struct Ord {
  Ordering (*compare)(const T& a, const T& b);
};

template<class Container, class Element>
const Element* maximum(
  const Index<Container, Element>& index,
  const Ord<Element>& ord,
  const Container& container) {

  const size_t size = index.size(container);
  const Element* max = nullptr;

  for (size_t i = 0; i < size; ++i) {
    const Element& current = index.at(container, i);
    if (!max || ord.compare(current, *max) == GT)
      max = &current;
  }

  return max;

}

Ponieważ parametry typu "typy fantomowe" nie są używane reprezentacyjnie, linker powinien mieć możliwość deduplikacji tego rodzaju funkcji, jeśli obawiasz się rozmiaru kodu. Typowa, niebezpieczna, ale może bardziej przyjazna dla kompilatora alternatywa byłaby do użycia void*.

W C ++ możesz także przekazać funkcje vtable jako parametry szablonu, jeśli znasz je w czasie kompilacji - to znaczy ręcznej debiutualizacji. To dopuszcza większą optymalizację (np. Inlining), ale oczywiście nie pozwala na dynamiczną wysyłkę.

Jedno zastrzeżenie: skoro nie masz aplikacji lub zamknięć częściowej funkcji, możesz uznać za interesujące doświadczenie częściowe specjalizacje, takie jak Haskell:

instance (Ord a) => Ord [a] where ...

Co mówi, że lista rzeczy [a] ma uporządkowanie, jeśli elementy a zamawiać.


3
2017-12-24 23:23