2015-12-02 25 views
32

Ecco una citazione dalla sezione di nota di implementazione di cppreference di std::shared_ptr, che indica che ci sono due diversi puntatori (come mostrato in grassetto): quello che può essere restituito da get() e quello che contiene i dati effettivi all'interno del blocco di controllo.Perché sono necessari due puntatori raw dell'oggetto gestito necessari nell'implementazione std :: shared_ptr?

In una tipica implementazione, std::shared_ptr vale soltanto due punti:

  1. il puntatore memorizzato (quello restituito dal get())
  2. un puntatore per controllare blocco

Il controllo il blocco è un oggetto assegnato dinamicamente che contiene:

  1. o un puntatore all'oggetto gestito o l'oggetto gestito stessa
  2. la delezione (tipo-cancellati)
  3. l'allocatore (tipo-cancellati)
  4. il numero di shared_ptrs che possiede l'oggetto gestito
  5. il numero di weak_ptrs che si riferiscono all'oggetto gestito

il puntatore tenuto dal shared_ptr è direttamente restituito da get(), mentre il puntatore o l'oggetto trattenuto dal blocco di controllo è quello che verrà eliminato quando il numero di proprietari condivisi raggiunge lo zero. Questi indicatori non sono necessariamente uguali.

La mia domanda è, perché sono necessari due puntatori diversi (i due in grassetto) necessari per l'oggetto gestito (oltre al puntatore del blocco di controllo)? Non è sufficiente quello restituito da get()? E perché questi indicatori non sono necessariamente uguali?

+0

IIUC implica che ci sono effettivamente, potenzialmente almeno, * tre * puntatori coinvolti: 1. Qualcosa restituito da get(); 2.Potenzialmente un puntatore * all'interno * del blocco di controllo verso l'oggetto che verrà eliminato alla fine; 3. Un puntatore al blocco di controllo. Due di questi sono detenuti dal ptr condiviso; il terzo risiede all'interno del blocco di controllo. –

+0

Questa è una domanda diversa, come ho sottolineato nel mio precedente commento. Non riguarda il blocco di controllo rispetto all'oggetto, ma il fatto che ci sono * due * puntatori su "l'oggetto" che possono anche essere diversi (o non ci sarebbe bisogno di tenerne due). * Più * il ptr al blocco di controllo. (E la risposta ha a che fare con il costruttore aliasing shared_ptr.) –

+0

Dalla mia comprensione, la risposta in http://stackoverflow.com/a/26351926 riguarda la differenza tra i due metodi di costruzione, ma non spiega ancora perché due possibili allocazioni diverse sono necessarie in primo luogo. – btshengsheng

risposta

37

La ragione di ciò è che è possibile avere uno shared_ptr che punta a qualcosa di diverso da quello che possiede e che è di progettazione. Questo è implementato usando il costruttore elencato come nr. 8 su cppreference:

template< class Y > 
shared_ptr(const shared_ptr<Y>& r, T *ptr); 

Un shared_ptr creato con questo costruttore azioni di proprietà con r, ma i punti di ptr. Considerate questo (ideato, ma illustrante) Codice:

std::shared_ptr<int> creator() 
{ 
    using Pair = std::pair<int, double>; 

    std::shared_ptr<Pair> p(new Pair(42, 3.14)); 
    std::shared_ptr<int> q(p, &(p->first)); 
    return q; 
} 

volta questa funzione uscite, solo un puntatore alla int subobject della coppia è disponibile al codice client. Ma a causa della proprietà condivisa tra q e p, il puntatore q "tiene in vita" l'intero oggetto Pair.

Una volta eseguito il dealloacation, il puntatore deve passare l'intero oggetto Pair al deleter. Quindi il puntatore all'oggetto Pair deve essere memorizzato da qualche parte accanto al deleter — in altre parole, nel blocco di controllo.

Per un esempio meno ingegnoso (probabilmente anche uno più vicino alla motivazione originale per la funzione), considerare il caso di puntare a una classe base. Qualcosa di simile a questo:

struct Base1 
{ 
    // ::: 
}; 

struct Base2 
{ 
    // ::: 
}; 

struct Derived : Base1, Base2 
{ 
// ::: 
}; 

std::shared_ptr<Base2> creator() 
{ 
    std::shared_ptr<Derived> p(new Derived()); 
    std::shared_ptr<Base2> q(p, static_cast<Base2*>(p.get())); 
    return q; 
} 

Naturalmente, l'effettiva attuazione di std::shared_ptr ha tutte le conversioni implicite in atto in modo che la danza p -e- q in creator non è necessario, ma ho tenuto lì per assomigliare il primo esempio.

+6

La conversione da classi derivate a classi base è meno inventata di un esempio. –

+10

Penso che l'esempio sia abbastanza carino così com'è. Il punto è che puntiamo a una parte di qualcosa che è gestita come un'entità atomica, e una coppia è probabilmente l'esempio più banale e non ambiguo. L'ereditarietà qui è solo una forma speciale di "essere parte di qualcosa". –

+1

@Angew: Non ti amo. Ew in C++. Il fatto che questo sia praticamente l'esempio più pulito possibile va solo a sostegno del mio punto! –

-1

Diamo un'occhiata a std::shared_ptr<int> Questo è un puntatore intelligente conteggiato con riferimento a un int*. Ora il int* non contiene informazioni sul conteggio dei riferimenti e l'oggetto shared_ptr non può contenere le informazioni di conteggio dei riferimenti poiché potrebbe essere distrutto molto prima che il conteggio dei riferimenti scenda a zero.

Ciò significa che è necessario disporre di un oggetto intermedio per mantenere le informazioni di controllo che rimangono permanenti finché il conteggio dei riferimenti non scende a zero.

Detto questo, se si crea shared_ptr con make_shared sia il int e il blocco di controllo verrà creato in memoria contigua rendendo il dereferenziazione molto più efficiente.

+1

Non hai risposto alla domanda poiché dalla tua spiegazione è sufficiente solo un puntatore al blocco di controllo, e 'get()' potrebbe semplicemente restituire il puntatore all'oggetto reale all'interno del blocco di controllo. – Slava

+3

Come ho capito la domanda (e ho cercato di chiarirlo), la domanda non è perché c'è "un puntatore al blocco di controllo" e "un puntatore all'oggetto". La domanda è: perché c'è "un puntatore al blocco di controllo", quindi "un puntatore all'oggetto, memorizzato all'interno di' shared_ptr' ", e quindi" un puntatore all'oggetto, memorizzato all'interno del blocco di controllo ". – Angew

+0

@ La nuova domanda è "perché ci sono DUE puntatori grezzi" quando uno è sufficiente – Slava

0

Un'esigenza imprescindibile per un blocco di controllo è supportare i puntatori deboli. Non è sempre possibile notificare tutti i punti deboli sulla distruzione di un oggetto (infatti, è quasi sempre non fattibile). Di conseguenza, i punti deboli hanno bisogno di qualcosa su cui puntare finché non se ne sono andati tutti. Quindi, un blocco di memoria deve rimanere in giro. Quel blocco di memoria è il blocco di controllo. A volte possono essere allocati insieme, ma allocarli separatamente consente di recuperare un oggetto potenzialmente costoso mantenendo il blocco di controllo economico.

La regola generale è che il blocco di controllo persiste finché esiste un singolo puntatore condiviso o un puntatore debole che fa riferimento ad esso, mentre l'oggetto può essere recuperato nell'istante in cui non ci sono puntatori condivisi che puntano a esso.

Ciò consente anche i casi in cui l'oggetto viene portato in proprietà condivisa dopo la sua allocazione. make_shared potrebbe essere in grado di unire questi due concetti in un blocco di memoria, ma è necessario che prima di allocare T sia necessario specificare shared_ptr<T>(new T) e quindi capire come condividerlo dopo il fatto. Quando ciò non è desiderabile, boost ha un concetto correlato di intrusive_ptr che fa il suo conteggio dei riferimenti direttamente all'interno dell'oggetto piuttosto che con un blocco di controllo (devi scrivere gli operatori di incremento e decremento per farlo funzionare).

Ho visto implementazioni di puntatori condivise che non hanno un blocco di controllo. Invece, i puntatori condivisi sviluppano una lista concatenata tra loro. Finché la lista collegata contiene 1 o più shared_ptrs, l'oggetto è ancora vivo. Tuttavia, questo approccio è più complicato in uno scenario di multithreading perché è necessario mantenere l'elenco collegato anziché un semplice conteggio ref. È probabile che il runtime sia anche peggiore in molti scenari in cui si assegnano e si riassegnano più volte shared_ptrs, poiché l'elenco collegato è più pesante.

È inoltre possibile che un'implementazione ad alte prestazioni del pool allochi i blocchi di controllo, portando il costo dell'utilizzo quasi a zero.

+1

La domanda è "Perché il blocco di controllo contiene una copia del puntatore memorizzato", non "Perché c'è un blocco di controllo?" –

Problemi correlati