Wird der Deleter eines shared_ptr im vom benutzerdefinierten Allokator zugewiesenen Speicher gespeichert?


22

Angenommen, ich habe eine shared_ptrmit einem benutzerdefinierten Allokator und einem benutzerdefinierten Löscher.

Ich kann im Standard nichts finden, das darüber spricht, wo der Deleter gespeichert werden soll: Es heißt nicht, dass der benutzerdefinierte Allokator für den Speicher des Deleters verwendet wird, und es heißt nicht, dass dies nicht der Fall sein wird.

Ist das nicht spezifiziert oder fehlt mir nur etwas?

Antworten:


11

util.smartptr.shared.const / 9 in C ++ 11:

Effekte: Erstellt ein shared_ptr-Objekt, dem das Objekt p und der Deleter d gehören. Der zweite und vierte Konstruktor verwenden eine Kopie von a, um Speicher für den internen Gebrauch zuzuweisen.

Der zweite und vierte Konstruktor haben diese Prototypen:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

Im neuesten Entwurf entspricht util.smartptr.shared.const / 10 für unseren Zweck:

Effekte: Erstellt ein shared_ptr-Objekt, dem das Objekt p und der Deleter d gehören. Wenn T kein Array-Typ ist, aktivieren der erste und der zweite Konstruktor shared_from_this mit p. Der zweite und vierte Konstruktor verwenden eine Kopie von a, um Speicher für den internen Gebrauch zuzuweisen. Wenn eine Ausnahme ausgelöst wird, wird d (p) aufgerufen.

Der Allokator wird also verwendet, wenn er im zugewiesenen Speicher zugeordnet werden muss. Basierend auf dem aktuellen Standard und bei relevanten Fehlerberichten ist die Zuordnung nicht obligatorisch, sondern wird vom Ausschuss übernommen.

  • Obwohl die Schnittstelle von shared_ptreine Implementierung ermöglicht, bei der es nie einen Steuerblock und alle gibt shared_ptrund weak_ptrdie in eine verknüpfte Liste aufgenommen werden, gibt es in der Praxis keine solche Implementierung. Darüber hinaus wurde der Wortlaut geändert, beispielsweise unter der Annahme, dass der Wortlaut use_countgeteilt wird.

  • Der Deleter muss sich nur konstruierbar bewegen. Somit ist es nicht möglich, mehrere Kopien in der zu haben shared_ptr.

Man kann sich eine Implementierung vorstellen, die den Deleter in ein speziell entworfenes setzt shared_ptrund ihn verschiebt, wenn das Special shared_ptrgelöscht wird. Die Implementierung scheint zwar konform zu sein, ist aber auch seltsam, zumal möglicherweise ein Steuerblock für die Anzahl der Verwendungen erforderlich ist (es ist möglicherweise möglich, aber noch seltsamer, dasselbe mit der Anzahl der Verwendungen zu tun).

Relevante DRs, die ich gefunden habe: 545 , 575 , 2434 (die bestätigen, dass alle Implementierungen einen Steuerblock verwenden und zu implizieren scheinen, dass Multithreading-Einschränkungen dies etwas vorschreiben ), 2802 (was erfordert, dass sich der Deleter nur konstruierbar bewegt und somit die Implementierung verhindert, wo Der Deleter wird zwischen mehreren kopiert shared_ptr.


2
"Speicher für den internen Gebrauch zuweisen" Was ist, wenn die Implementierung zunächst keinen Speicher für den internen Gebrauch zuweisen wird? Es kann ein Mitglied verwenden.
LF

1
@LF Es kann nicht, die Schnittstelle erlaubt das nicht.
AProgrammer

Theoretisch kann es immer noch eine Art "Small Deleter-Optimierung" verwenden, oder?
LF

Was seltsam ist, ist, dass ich nichts über die Verwendung des gleichen Allokators (Kopie von a) zum Freigeben dieses Speichers finden kann. Was bedeuten würde, dass diese Kopie von gespeichert wird a. Es gibt keine Informationen darüber in [util.smartptr.shared.dest].
Daniel Langr

1
@DanielsaysreinstateMonica, ich frage mich, ob in util.smartptr.shared / 1: "Die Klassenvorlage shared_ptr speichert einen Zeiger, der normalerweise über new erhalten wird. Shared_ptr implementiert die Semantik des gemeinsamen Besitzes; der letzte verbleibende Besitzer des Zeigers ist für die Zerstörung des Objekts verantwortlich. oder auf andere Weise die mit dem gespeicherten Zeiger verknüpften Ressourcen freizugeben. " Das Freigeben der dem gespeicherten Zeiger zugeordneten Ressourcen ist dafür nicht vorgesehen. Der Steuerblock sollte aber auch überleben, bis der letzte schwache Zeiger gelöscht wird.
AProgrammer

4

Von std :: shared_ptr haben wir:

Der Steuerblock ist ein dynamisch zugewiesenes Objekt, das Folgendes enthält:

  • entweder ein Zeiger auf das verwaltete Objekt oder das verwaltete Objekt selbst;
  • der Deleter (Typ gelöscht);
  • der Allokator (typgelöscht);
  • die Anzahl der shared_ptrs, denen das verwaltete Objekt gehört;
  • Die Anzahl der schwachen_Ptrs, die auf das verwaltete Objekt verweisen.

Und von std :: allocate_shared erhalten wir:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Konstruiert ein Objekt vom Typ T und verpackt es in ein std :: shared_ptr [...], um eine Zuordnung sowohl für den Steuerblock des gemeinsam genutzten Zeigers als auch für das T-Objekt zu verwenden.

Es sieht also so aus, als ob std :: allocate_shared das deletermit Ihrem zuordnen sollte Alloc.

n4810BEARBEITEN : Und ab §20.11.3.6 Erstellung [util.smartptr.shared.create]

1 Die gemeinsamen Anforderungen , die für alle gelten make_shared, allocate_shared, make_shared_default_init, und allocate_shared_default_initÜberlastungen, wenn nicht anders angegeben, werden im Folgenden beschrieben.

[...]

7 Anmerkungen: (7.1) - Implementierungen sollten nicht mehr als eine Speicherzuweisung durchführen. [Hinweis: Dies bietet eine Effizienz, die einem aufdringlichen intelligenten Zeiger entspricht. - Endnote]

[Hervorhebung aller meiner]

Also der Standard sagt , dass std::allocate_shared sollte verwendet werden Allocfür den Steuerblock.


1
Es tut mir leid, dass cppreference kein normativer Text ist. Es ist eine großartige Ressource, aber nicht unbedingt für Fragen von Sprachanwälten .
Geschichtenerzähler - Unslander Monica

@ StoryTeller-UnslanderMonica Stimme voll und ganz zu - habe den neuesten Standard durchgesehen und konnte nichts finden, also ging es mit cppreference.
Paul Evans


Gefundene n4810und aktualisierte Antwort.
Paul Evans

1
Hier geht es jedoch make_sharednicht um die Konstruktoren selbst. Trotzdem kann ich ein Mitglied für kleine Löscher verwenden.
LF

3

Ich glaube, das ist nicht spezifiziert.

Hier ist die Spezifikation der relevanten Konstruktoren: [util.smartptr.shared.const] / 10

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Effekte: Konstruiert ein shared_­ptrObjekt, dem das Objekt pund der Deleter gehören d. Wenn Tes sich nicht um einen Array-Typ handelt, aktivieren der erste und der zweite Konstruktor shared_­from_­thismit p. Der zweite und vierte Konstruktor verwenden eine Kopie von a, um Speicher für den internen Gebrauch zuzuweisen . Wenn eine Ausnahme ausgelöst wird, d(p)wird aufgerufen.

Meine Interpretation lautet nun, dass die Implementierung, wenn sie Speicher für den internen Gebrauch benötigt, dies mithilfe von verwendet a. Dies bedeutet nicht, dass die Implementierung diesen Speicher verwenden muss, um alles zu platzieren. Angenommen, es gibt diese seltsame Implementierung:

template <typename T>
class shared_ptr : /* ... */ {
    // ...
    std::aligned_storage<16> _Small_deleter;
    // ...
public:
    // ...
    template <class _D, class _A>
    shared_ptr(nullptr_t, _D __d, _A __a) // for example
        : _Allocator_base{__a}
    {
        if constexpr (sizeof(_D) <= 16)
            _Construct_at(&_Small_deleter, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

Verwendet diese Implementierung "eine Kopie von a, um Speicher für den internen Gebrauch zuzuweisen"? Ja tut es. Es wird niemals Speicher zugewiesen, außer durch Verwendung a. Es gibt viele Probleme mit dieser naiven Implementierung, aber sagen wir, dass sie auf die Verwendung von Allokatoren umschaltet, außer im einfachsten Fall, in dem der shared_ptrdirekt aus einem Zeiger erstellt wird und niemals kopiert oder verschoben oder auf andere Weise referenziert wird und es keine anderen Komplikationen gibt. Der Punkt ist, nur weil wir uns eine gültige Implementierung nicht vorstellen können, beweist sie nicht, dass sie theoretisch nicht existieren kann. Ich sage nicht, dass eine solche Implementierung tatsächlich in der realen Welt zu finden ist, nur dass der Standard sie nicht aktiv zu verbieten scheint.


IMO Ihr shared_ptrfür kleine Typen reserviert Speicher auf dem Stapel. Und erfüllt damit nicht die Standardanforderungen
Bartop

1
@bartop Es wird kein Speicher auf dem Stapel "zugewiesen". _Smaller_deleter ist unbedingt Teil der Darstellung eines shared_ptr. Das Aufrufen eines Konstruktors in diesem Bereich bedeutet nicht, etwas zuzuweisen. Andernfalls zählt sogar das Halten eines Zeigers auf den Steuerblock als "Speicher zuweisen", oder? :-)
LF

Der Deleter muss jedoch nicht kopierbar sein. Wie würde dies funktionieren?
Nicol Bolas

@NicolBolas Umm ... Verwenden Sie std::move(__d)und greifen Sie zurück, allocatewenn eine Kopie erforderlich ist.
LF
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.