Eine Zeiger-Halter Klasse, ist sowas sinnvoll? Was für Probleme entstehen?
-
Ab und an kommt es mal vor, dass man in einer Klasse dynamisch Speicher alloziert. Meistens benutzt dafür ja vorgefertigte Lösungen wie Container oder Smart-Pointer. Wenn aber das zu allozierende Objekt nur intern benötigt wird, wäre ein shared_ptr wohl fehl am Platze. auto_ptr auch, da man beim Kopieren ja immer noch Besitzer des Objekts bleiben will. Beim Kopieren sollte also eine Kopie des tatsächlichen Objekts gemacht werden. So wie es z.B. die Standard Container beim Kopiern machen. scoped_ptr ist noncopyable also auch nicht für ein Objekt, dass man Kopieren können soll geeignet.
Ist es in so einem Fall sinvoll eine Klasse zu schreiben, die einen Zeiger "hält", ihn im Destruktor freigibt und beim Aufruf des Kopierkonstruktors bzw. Zuweisungsoperators das tatsächliche Objekt kopiert? Um Slicing zu verhindern, könnte man verschiedene Clone-Policies anbieten. Ein Probelm ist, dass das Ganze mit unvollständigen Typen nur teilweise funktioniert (Destruktoraufruf).
Hier mal eine grobe Implementierung, wie ich mir so etwas vorstelle:
template <class HeldType, template <typename> class Cloner = NewCopy> class PointerHolder { typedef Cloner<HeldType> Cloner; public: PointerHolder(HeldType* pointer) : pointer(pointer) { } PointerHolder(PointerHolder const& other) : pointer(Cloner::clone(other.pointer)) { } ~PointerHolder() { delete pointer; } void operator = (PointerHolder other) { this->swap(other); } void swap(PointerHolder& with) { std::swap(pointer, with.pointer); } private: HeldType* pointer; };Der Cloner NewCopy erstellt nur eine flache Kopie, man könnte aber schnell eine Policy für tiefe Kopien schreiben (unter der Voraussetzung der gegebene Typ unterstützt ein definiertes Interface (z.B. eine virtuelle new_clone Methode):
template <class Type> class NewClone { public: static Type* clone(Type* ptr) { return ptr->new_clone(); } };Der Vorteil wäre, dass man in Klassen die so einen Zeiger-Halter verwenden die großen Drei nicht mehr implementieren müsste, bzw. die Implementierung trivial ist.
Ich denke aber mal, dass es mit einem solchen Design viele Probleme gibt, da ich selbst auch nur wenig darüber nachgedacht habe. Wenn so etwas sinnvoll wäre, wäre es doch schon längst in Bibliotheken wie boost zu finden, oder habe ich es einfach übersehen?
Gruß
Don06
-
Klar, macht Sinn.
Wir nur selten gebraucht, aber macht Sinn.
-
Don06 schrieb:
Wenn so etwas sinnvoll wäre, wäre es doch schon längst in Bibliotheken wie boost zu finden, oder habe ich es einfach übersehen?
Im Prinzip gehöre ich zu denen die mit den scoped_ptr/shared_ptr/weak_ptr bislang auskommen, aber es gibt ja auch andere Bibliothekten die noch grundsätzlich andere (oder stärker konfigurierbare) Smartpointer unterstützen - siehe z.B. die Loki-Bibliothek. Gründe gibt es daher wohl (Beispielsweise für COM-Interfaces...).
Wobei ich auch ohne dein Konstrukt mit dem shared_ptr und einem recht trivialen Kopier- und Zuweisungsoperator auskomme (Verwende das z.B. beim Handle-Body Idiom recht häufig).
cu André
-
asc schrieb:
Wobei ich auch ohne dein Konstrukt mit dem shared_ptr und einem recht trivialen Kopier- und Zuweisungsoperator auskomme (Verwende das z.B. beim Handle-Body Idiom recht häufig).
shared_ptr wird imho einfach zu oft eingesetzt.
es ist zwar gut dass die leute endlich keine rohen zeiger mehr anfassen aber dafuer wird jetzt immer shared_ptr als universalloesung verwendet
-
Shade Of Mine schrieb:
shared_ptr wird imho einfach zu oft eingesetzt.
es ist zwar gut dass die leute endlich keine rohen zeiger mehr anfassen aber dafuer wird jetzt immer shared_ptr als universalloesung verwendet
Es gibt im Standard keine leichtgewichtigere Alternative die a) mit einer Forward-Declaration auskommt und b) nicht als "deprecated" markiert ist. Ja, die Nachteile des shared_ptr sind mir auch ein Begriff, nur dennoch verwende ich ihn als die IMHO bessere Alternative zum rohen Zeiger.
-
Wäre denn eine eigene Implementation eines solchen Smartpointers sehr schwierig? Ich hab drum noch nie was ähnliches gemacht und kenne mich auch nicht sehr gut mit Smartpointern aus, deshalb kann ich es nur schlecht einschätzen...
-
Mich würde da vor allem interessieren, wie man es schaffen kann unvollständige Typen richtig zu zerstören. Wie schafft boost das? Ich blick da irgendwie nicht so durch, shared_count, sp_counted_base, get_internal_deleter? keine Ahnung.
-
Don06 schrieb:
Mich würde da vor allem interessieren, wie man es schaffen kann unvollständige Typen richtig zu zerstören. Wie schafft boost das? Ich blick da irgendwie nicht so durch, shared_count, sp_counted_base, get_internal_deleter? keine Ahnung.
Sind die Typen nicht nur bei der Deklaration des Smartpointers unvollständig (z.B. um sie mit Vorwärtsdeklarationen in Klassen benutzen zu können)?
deletekann man ja auf einen Zeiger durchführen, und der Templatetyp ist dann normalerweise definiert (man initialisiert Smartpointer ja mitnew)...
-
@Don06:
Wie boost das macht ist eigentlich einfach, aber irgendwie schwer zu erklären.
Man kann sagen dass beim Initialisieren eines shared_ptr automatisch ein "deleter" erzeugt wird. Dadurch muss der Typ nur überall dort vollständig sein wo ein shared_ptr auch initialisiert wird.Zusätzlich verwendet die Boost ein Konstrukt welches sicherstellt dass der Typ auch vollständig ist wenn dieser "deleter" erzeugt wird.
Was z.B. mit einem Boost shared_ptr nicht gehe wird ist sowas:
class foo; // unvollständig foo* bar(); void baz() { shared_ptr<foo> p(bar()); // <- hier würde der deleter erzeugt, und hier wird der compiler einen fehler melden, da der typ unvollständig ist }Wie sowas funktionieren kann kann man an einem einfachen Beispiel schnell sehen:
template <class T> void my_checked_delete(T const* t) { sizeof(T); // müsste IMO reichen damit es knallt wenn T nicht vollständig ist delete t; } template <class T> class foo : boost::noncopyable { public: explicit foo(T* t) : m_t(t), m_deleter(&my_checked_delete<T>) {} ~foo() { (*m_deleter)(m_t); } private: T* m_t; void (*m_deleter)(T const* t); // der deleter };
-
@hustbaer: cool, danke. Das ist doch recht verständlich. Wenn ich also in einer Klasse das Body-Handle-Idiom verwende, ist es nicht ohne Tricks machbar, die großen Drei automatisch erzeugen zu lassen, da die Implementierungsklasse einen unvollständigen Typ hat.
class MyClass { MyClass(); private: class Impl; owned_ptr<Impl> pImpl; }Ich muss also in jedem Fall die Implementierung von Kopierkonstruktor, Destruktor und Zuweisungsoperator in der Quelldatei vornehmen, damit der interne Implementierungstype bekannt ist.
Habe ich das soweit richtig verstanden?
Gruß
Don06
-
Ich muss also in jedem Fall die Implementierung von Kopierkonstruktor, Destruktor und Zuweisungsoperator in der Quelldatei vornehmen, damit der interne Implementierungstype bekannt ist.
Habe ich das soweit richtig verstanden?
Naja was den dtor angeht kannst du es so machen wie shared_ptr, dann kannst du den dtor im Header File implementieren ohne dass der Typ dort vollständig sein muss. Bzw. gleich shared_ptr verwenden. Oder deine owned_ptr Klasse so basteln dass sie einen entsprechenden Mechanismus verwendet damit nur bei der Initialisierung der Typ bekannt sein muss, nicht aber im dtor.
Was copy-ctor und assignment-operator angeht - wenn du ne deep-copy machst, dann muss der Typ natürlich vollständig sein, sonst wirst du ihn nicht kopieren können. Für shallow-copy natürlich nicht.
Und natürlich lässt sich das selbe Grundprinzip auch anwenden um Kopien zu erstellen. z.B.:
template <class T> struct clone_ptr_ops { static void do_delete(T const* p) { boost::checked_delete(p); } static void* do_clone(T const* p) { return new T(*p); } }; namespace detail { struct clone_ptr_ops_table { void (*do_delete)(void const*); void* (*do_clone)(void const*); }; inline void nop_deleter(void const*) {} inline void* nop_cloner(void const*) { return 0; } static const clone_ptr_ops_table clone_ptr_nop_ops_table = { &nop_deleter, &nop_cloner }; template <class OPS, class U> class clone_ptr_table_holder { static void delete_adapter(void const* p) { OPS::do_delete(static_cast<U const*>(p)); } static void* clone_adapter(void const* p) { return p ? OPS::do_clone(static_cast<U const*>(p)) : 0; } public: static clone_ptr_ops_table const* get_table() { static const clone_ptr_ops_table table = { &delete_adapter, &clone_adapter }; return &table; } }; } // namespace detail template <class T> class clone_ptr { public: clone_ptr() : m_p(0), m_ops(&detail::clone_ptr_nop_ops_table) {} template <class U> explicit clone_ptr(U* u) : m_p(u), m_ops(detail::clone_ptr_table_holder<clone_ptr_ops<U>, U>::get_table()) {} ~clone_ptr() { m_ops->do_delete(m_p); } clone_ptr(clone_ptr const& other) : m_p(0), m_ops(&detail::clone_ptr_nop_ops_table) { T* clone = static_cast<T*>(other.m_ops->do_clone(other.m_p)); m_p = clone; m_ops = other.m_ops; } clone_ptr& operator =(clone_ptr const& other) { clone_ptr(other).swap(*this); return *this; } template <class U> void reset(U* u) { clone_ptr(u).swap(*this); } void swap(clone_ptr& other) { std::swap(m_p, other.m_p); std::swap(m_ops, other.m_ops); } T* operator -> () { return m_p; } T const* operator -> () const { return m_p; } T* get() { return m_p; } T const* get() const { return m_p; } private: T* m_p; detail::clone_ptr_ops_table const* m_ops; };
-
Don06 schrieb:
Ich muss also in jedem Fall die Implementierung von Kopierkonstruktor, Destruktor und Zuweisungsoperator in der Quelldatei vornehmen, damit der interne Implementierungstype bekannt ist.
Habe ich das soweit richtig verstanden?
Richtig. Es würde sowieso nur in sehr wenigen Fällen Sinn machen, die Implementierung der gesamten Klasse inklusive Membern vor den Klienten zu verstecken (das ist schließlich der Sinn des pimpl-idioms) und dann die Implementierung der Big3 doch in den Header zu packen...