2015-10-22 4 views
35

Ich lese "Effective Modern C++". In dem Artikel, der sich auf std::unique_ptr bezieht, heißt es, dass, wenn der benutzerdefinierte Löscher ein zustandsloses Objekt ist, keine Größengebühren anfallen, aber wenn es sich um einen Funktionszeiger oder um eine std::function Gebühr handelt. Können Sie erklären warum?C++ std :: unique_ptr: Warum gibt es bei lambdas keine Größengebühren?

Lassen Sie uns sagen, dass wir den folgenden Code haben:

auto deleter_ = [](int *p) { doSth(p); delete p; }; 
std::unique_ptr<int, decltype(deleter_)> up(new int, deleter_); 

Zu meinem Verständnis, das unique_ptr ein Objekt vom Typ sollte decltype(deleter_) und weisen deleter_ zu diesem internen Objekt. Aber offensichtlich passiert das nicht. Können Sie den Mechanismus dafür anhand eines möglichst kleinen Codebeispiels erklären?

+5

Die Implementierung kann die leere Basisklasse Optimierung auf ihre internen Daten verwenden, wodurch ein Das staatenlose Deleter-Objekt "verschwindet", indem es eine andere Struktur daraus ableitet. –

+0

@BoPersson OK, ich kann nicht herausfinden, wie diese Technik hier verwendet wird. Könnten Sie eine Antwort darauf schreiben? Vielen Dank. –

+2

Ok, ich habe ein Beispiel hinzugefügt. –

Antwort

19

Wenn der Deleter staatenlos ist, ist kein Speicherplatz zum Speichern erforderlich. Wenn der Deleter nicht zustandslos ist, muss der Zustand im unique_ptr selbst gespeichert werden.
std::function und Funktionszeiger haben Informationen, die nur zur Laufzeit verfügbar sind und daher im Objekt neben dem Zeiger das Objekt selbst gespeichert werden müssen. Dies wiederum erfordert das Zuordnen (in dem unique_ptr selbst) Speicherplatz, um diesen zusätzlichen Status zu speichern.

Vielleicht verstehen Sie die Empty Base Optimization wird Ihnen helfen zu verstehen, wie dies in der Praxis umgesetzt werden könnte.
Das Merkmal std::is_empty ist eine andere Möglichkeit, wie dies implementiert werden könnte.

Wie genau Bibliotheksautoren das implementieren, liegt natürlich bei ihnen und was der Standard erlaubt.

+0

Ich habe meine Frage bearbeitet. Kannst du es dir anschauen? –

+0

@ jnbrq-CanberkSönmez Der Mechanismus ist sehr gut von vielen Leuten erklärt, so habe ich Sie nur mit einigen weiteren Informationen zu diesem Thema verbunden. Hoffentlich gibt dies Ihnen die Schlüsseleinblicke, die benötigt werden, um zu visualisieren, wie dies in der Sprache erreicht werden kann. – SirGuy

13

Aus unique_ptr Implementierung:

template<class _ElementT, class _DeleterT = std::default_delete<_ElementT>> 
class unique_ptr 
{ 
public: 
    // public interface... 

private: 

    // using empty base class optimization to save space 
    // making unique_ptr with default_delete the same size as pointer 

    class _UniquePtrImpl : private deleter_type 
    { 
    public: 
    constexpr _UniquePtrImpl() noexcept = default; 

    // some other constructors... 

    deleter_type& _Deleter() noexcept 
    { return *this; } 

    const deleter_type& _Deleter() const noexcept 
    { return *this; } 

    pointer& _Ptr() noexcept 
    { return _MyPtr; } 

    const pointer _Ptr() const noexcept 
    { return _MyPtr; } 

    private: 
    pointer _MyPtr; 

    }; 

    _UniquePtrImpl _MyImpl; 

}; 

Die _UniquePtrImpl Klasse enthält den Zeiger und leitet sich von dem deleter_type.

Wenn der Deleter statuslos ist, kann die Basisklasse optimiert werden, so dass sie keine Bytes für sich selbst benötigt. Dann kann die ganze unique_ptr die gleiche Größe wie der enthaltene Zeiger haben - das heißt: die gleiche Größe wie ein gewöhnlicher Zeiger.

+0

Also kann ich sagen, dass 'deleter_type'' declltype (deleter_) 'ist, wo' deleter_' ein statusloses Lambda ist, dann ist 'deleter_type' eine leere Klasse? –

+0

Was ist, wenn deleter_type endgültig ist? –

+0

@KarlisOlte, in der Tat, echte Implementierungen müssen ein bisschen schlauer sein und erkennen diesen Fall. –

33

Ein unique_ptr muss immer seinen Deleter speichern. Wenn es sich bei dem Deleter um einen Klassentyp ohne Status handelt, kann unique_ptrempty base optimization verwenden, sodass der Deleter keinen zusätzlichen Speicherplatz verwendet.

Wie genau dies geschieht, unterscheidet sich zwischen Implementierungen. Zum Beispiel speichern sowohl libc++ als auch MSVC den verwalteten Zeiger und den Deleter in einer compressed pair, wodurch Sie automatisch eine leere Basisoptimierung erhalten, wenn einer der beteiligten Typen eine leere Klasse ist.

Von dem libC++ Link oben

template <class _Tp, class _Dp = default_delete<_Tp> > 
class _LIBCPP_TYPE_VIS_ONLY unique_ptr 
{ 
public: 
    typedef _Tp element_type; 
    typedef _Dp deleter_type; 
    typedef typename __pointer_type<_Tp, deleter_type>::type pointer; 
private: 
    __compressed_pair<pointer, deleter_type> __ptr_; 

libstdC++ stores the two in einem std::tuple und einig Google-Suche schlägt ihre tuple Implementierung Optimierung leer Basis beschäftigt, aber ich kann keine Dokumentation nicht finden, so ausdrücklich erklärt.

In jedem Fall zeigt this example, dass sowohl libC++ als auch libstdC++ EBO verwenden, um die Größe eines unique_ptr mit einem leeren Deleter zu reduzieren.

+2

Beachten Sie, dass, wenn der Zeiger ein leeres Objekt des gleichen Typs wie 'deleter_type' ist, der obige Code die beiden nicht komprimieren kann. Außer dass nur ein Idiot das tun würde. ;) – Yakk

+0

@Yakk: Habe noch keinen Weg gefunden, einen Zeiger zu einem leeren Objekt zu machen ... – Deduplicator

+3

@Deduplicator Ich nehme an, du könntest einen leeren Typ haben, der * NullablePointer * erfüllt und auch als No-Op Deleter fungiert. Es wäre wahrscheinlich das nutzloseste "unique_ptr", das jemals erstellt wurde, aber Yakks Kommentar gilt in diesem Fall :) – Praetorian

5

Tatsächlich wird eine Größenstrafe für Lambdas sein, die nicht zustandslos sind, d. H. Lambdas, die einen oder mehrere Werte erfassen.

Aber für Nicht-Erfassung lambdas gibt es zwei wichtige Fakten zu bemerken:

  • Der Typ des einzigartigen und bekannt für den Compiler nur Lambda.
  • Nicht einfangende Lambdas sind zustandslos.

Daher ist der Compiler in der Lage des Lambda rein auf seiner Typ Basis aufzurufen, die als Teil von der Art des unique_ptr aufgezeichnet wird; kein extra Laufzeit Informationen sind erforderlich.

Dies ist in der Tat, warum nicht einfangende Lambdas zustandslos sind. In Bezug auf die Frage der Größenbeschränkung gibt es natürlich nichts Besonderes an nicht-erfassenden Lambdas im Vergleich zu irgendeinem anderen statuslosen Lösch-Funktortyp.

Beachten Sie, dass std::function ist nicht staatenlos, weshalb die gleiche Argumentation tut nicht für sie gelten.

Schließlich müssen statusfreie Objekte in der Regel eine Größe ungleich Null aufweisen, um sicherzustellen, dass sie eindeutige Adressen haben. Stateless-Basisklassen sind nicht erforderlich, um die Gesamtgröße des abgeleiteten Typs hinzuzufügen; Dies wird als leere Basisoptimierung bezeichnet. So kann unique_ptr (wie in der Antwort von Bo Perrson) als ein Typ implementiert werden, der vom Deletertyp abgeleitet ist, der, wenn er zustandslos ist, keine Größenstrafe mit sich bringt. (Dies kann in der Tat die nur Weg unique_ptr ohne eine Größenstrafe für statuslose Deleters zu implementieren, aber ich bin mir nicht sicher.)

+2

Der Ansatz von libstdC++ besteht darin, standardmäßig std :: tuple für leere Basisoptimierung zu verwenden, anstatt überall leere Basisklassen explizit zu stapeln. – oakad

+0

@oakad Das klingt viel eleganter. –

+0

... obwohl die meisten oder alle std. :: Tuple-Implementierungen ohnehin auf Vererbung angewiesen sind, oder? –