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?
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.
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.
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
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
Tak więc, w razie potrzeby, używaj wektora
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.
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.
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
Nie rób tego. To neguje polimorfizm, który próbujesz / align = "left" /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.
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.
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.
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:
- 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.
- 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.
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