Czy Mogę mieć kontenery polimorficzne z semantyką wartości w C++?

Ogólnie rzecz biorąc, wolę używać wartości zamiast semantyki wskaźnika w C++ (tj. używać vector<Class> zamiast vector<Class*>). Zazwyczaj niewielka utrata wydajności jest bardziej niż rekompensowana przez brak konieczności pamiętania o dynamicznym usuwaniu przydzielanych obiektów.

Niestety, Kolekcje wartości nie działają, jeśli chcesz przechowywać różne typy obiektów, które wszystkie pochodzą ze wspólnej bazy. Zobacz przykład poniżej.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

Moje pytanie brzmi: Czy Mogę mieć swoje ciasto (semantyka wartości) i jeść to też (pojemniki polimorficzne)? Czy muszę używać wskaźników?

 31
Author: Agustin Meriles, 2008-09-03

9 answers

Ponieważ obiekty różnych klas będą miały różne rozmiary, pojawi się problem z krojeniem, jeśli zapiszesz je jako wartości.

Rozsądnym rozwiązaniem jest przechowywanie inteligentnych wskaźników bezpiecznych w pojemnikach. Zwykle używam boost:: shared_ptr, który można bezpiecznie przechowywać w kontenerze. Zauważ, że std::auto_ptr nie jest.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

Shared_ptr używa zliczania referencji, więc nie usunie podstawowej instancji, dopóki wszystkie referencje nie zostaną usunięte.

 24
Author: 1800 INFORMATION,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-03 02:19:53

Tak, możesz.

Doładowanie.biblioteka ptr_container dostarcza semantyczne wersje wartości polimorficznych standardowych kontenerów. Wystarczy przekazać wskaźnik do obiektu przydzielonego stercie, a kontener przejmie własność i wszystkie dalsze operacje zapewnią semantykę wartości, z wyjątkiem odzyskiwania własności, co daje prawie wszystkie zalety semantyki wartości za pomocą inteligentnego wskaźnika.

 10
Author: ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-04 21:39:09

Chciałem tylko zaznaczyć, że wektor jest zwykle bardziej wydajny niż wektor. W wektorze wszystkie Foo będą sąsiadowały ze sobą w pamięci. Przy założeniu zimnego TLB i bufora, pierwszy odczyt doda stronę do TLB i pociągnie fragment wektora do buforów L#; kolejne odczyty będą używać ciepłego bufora i załadowanego TLB, z sporadycznymi brakami bufora i rzadszymi błędami TLB.

Kontrast z wektorem : gdy wypełniasz wektor, otrzymujesz Foo * ' s z pamięci. Zakładając, że Twój alokator nie jest wyjątkowo inteligentny, (tcmalloc?) lub wypełniasz wektor powoli w czasie, lokalizacja każdego Foo prawdopodobnie będzie daleko od innych Foo: może tylko o setki bajtów, może megabajtów od siebie.

W najgorszym przypadku, gdy przeskanujesz wektor i zdereferujesz każdy wskaźnik, spowodujesz błąd TLB i brak pamięci podręcznej-to skończy się jako lot wolniejszy niż gdybyś miał wektor. (Cóż, w naprawdę w najgorszym przypadku każda Foo została wysłana na dysk, a każdy odczyt wiąże się z disk seek () I read (), aby przenieść stronę z powrotem do pamięci RAM.)

Tak więc, w razie potrzeby, używaj wektora. :-)

 10
Author: 0124816,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-16 11:08:33

Większość typów kontenerów chce abstrakcji konkretnej strategii przechowywania, czy to listy połączone, wektor, drzewo oparte lub co masz. Z tego powodu będziesz miał problemy zarówno z posiadaniem, jak i spożywaniem wspomnianego ciasta (tort jest kłamstwem (NB: ktoś musiał zrobić ten żart)).

Więc co robić? Cóż, istnieje kilka uroczych opcji, ale większość zmniejszy się do wariantów na jednym z kilku tematów lub ich kombinacji: wybieranie lub wymyślanie odpowiedniego inteligentnego wskaźnika, bawiąc się szablonami lub szablonami w sprytny sposób, używając wspólnego interfejsu dla kontenerów, który zapewnia hook do implementacji podwójnej wysyłki dla każdego kontenera.

Istnieje podstawowe napięcie między twoimi dwoma określonymi celami, więc powinieneś zdecydować, czego chcesz, a następnie spróbować zaprojektować coś, co da ci w zasadzie to, czego chcesz. To jest możliwe, aby zrobić kilka fajnych i nieoczekiwanych sztuczek, aby wskaźniki wyglądały jak wartości z wystarczająco sprytnym liczeniem referencji i wystarczająco sprytnym wdrożenie fabryki. Podstawową ideą jest użycie zliczania referencji i kopiowania na żądanie i stałości oraz (dla czynnika) kombinacji preprocesora, szablonów i statycznych reguł inicjalizacji C++, aby uzyskać coś, co jest tak inteligentne, jak to możliwe o automatyzacji konwersji wskaźników.

W przeszłości spędziłem trochę czasu próbując wyobrazić sobie, jak użyć wirtualnego Proxy/koperty-listu / tej uroczej sztuczki z liczonymi wskaźnikami odniesienia, aby osiągnąć coś w rodzaju podstawy dla wartościowego programowania semantycznego w C++.

I myślę, że można to zrobić, ale musiałbyś zapewnić dość zamknięty, zarządzany przez C#świat podobny do kodu w C++ (choć taki, z którego można przebić się do bazowego C++ w razie potrzeby). Więc mam dużo współczucia dla twojej linii myślenia.

 3
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-03 02:58:41

Możesz również rozważyć boost:: any . Używałem go do heterogenicznych pojemników. Podczas odczytu wartości z powrotem, musisz wykonać any_cast. Rzuci bad_any_cast, jeśli się nie powiedzie. Jeśli tak się stanie, możesz złapać i przejść do następnego typu.

I wierzę to rzuci bad_any_cast jeśli spróbujesz any_cast pochodnej klasy do jej bazy. Próbowałem:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

Wszystko, co jest powiedziane, chciałbym również przejść trasę shared_ptr pierwszy! Po prostu pomyślałem, że to może być interesujące.

 3
Author: Adam Hollidge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-03 12:02:36

Spójrz na static_cast i reinterpret_cast
W języku programowania C++, 3rd ed, Bjarne Stroustrup opisuje go na stronie 130. Jest cała sekcja na ten temat w rozdziale 6.
Możesz zmienić klasę rodziców na dziecięcą. To wymaga, aby wiedzieć, kiedy każdy z nich jest który. W książce dr Stroustrup mówi o różnych technikach, aby uniknąć tej sytuacji.

Nie rób tego. To neguje polimorfizm, który próbujesz / align = "left" /
 2
Author: 17 of 26,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-03 02:29:26

Aby dodać jedną rzecz do wszystkich 1800 informacji już powiedział.

Warto przyjrzeć się"bardziej efektywnemu C++" autorstwa Scotta Mayersa "Pozycja 3: nigdy nie traktuj tablic polimorficznie", aby lepiej zrozumieć ten problem.

 2
Author: Serge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:10

Używam własnej klasy template collection z semantyką exposed value type, ale wewnętrznie przechowuje wskaźniki. Używa niestandardowej klasy iterator, która po dereferenced otrzymuje odniesienie do wartości zamiast wskaźnika. Kopiowanie kolekcji powoduje głębokie kopie elementów, zamiast powielanych wskaźników, i to jest miejsce, w którym leży większość kosztów (naprawdę drobny problem, biorąc pod uwagę to, co dostaję zamiast).

To pomysł, który spełni Twoje potrzeby.

 1
Author: Johann Gerell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-16 11:24:19

Szukając odpowiedzi na ten problem, natknąłem się zarówno na to, jak i podobne pytanie. W odpowiedziach na inne pytanie znajdziesz dwa sugerowane rozwiązania:

  1. użyj std:: optional lub boost:: optional i wzorca odwiedzającego. To rozwiązanie utrudnia dodawanie nowych typów, ale ułatwia dodawanie nowych funkcjonalności.
  2. Użyj klasy wrapper podobnej do tego, co Sean Parent prezentuje w swoim przemówieniu. Takie rozwiązanie utrudnia dodawanie nowych funkcjonalność, ale łatwe dodawanie nowych typów.

Wrapper definiuje interfejs potrzebny dla klas i zawiera wskaźnik do jednego z takich obiektów. Implementacja interfejsu odbywa się za pomocą darmowych funkcji.

Oto przykładowa implementacja tego wzorca:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

Różne kształty są następnie implementowane jako zwykłe struktury/klasy. Możesz wybrać, czy chcesz korzystać z funkcji Członkowskich lub bezpłatnych funkcji (ale będziesz musiał zaktualizować powyższe implementacja do korzystania z funkcji Członkowskich). Preferuję darmowe funkcje:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

Możesz teraz dodać zarówno Circle, jak i Rectangle do tego samego std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

Minusem tego wzorca jest to, że wymaga dużej ilości kotła w interfejsie, ponieważ każda funkcja musi być zdefiniowana trzy razy. Plusem jest to, że otrzymujesz semantykę kopiowania:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

To daje:

Drew rectangle with width 2
Drew rectangle with width 2

Jeśli obawiasz się shared_ptr, możesz go zastąpić unique_ptr. Jednakże, nie będzie już można go kopiować i będziesz musiał przenieść wszystkie obiekty lub zaimplementować kopiowanie ręcznie. Sean Parent omawia to szczegółowo w swoim wystąpieniu, a implementacja jest pokazana we wspomnianej powyżej odpowiedzi.

 0
Author: dragly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-08 17:46:27