2014-09-24 18 views
5

Se si utilizza C++ std :: map (e altri contenitori) con tipi di valore, si noterà che l'inserimento nella mappa chiama il distruttore per il tipo di elemento. Questo perché l'implementazione dell'operatore [] è richiesto dalle specifiche C++ per essere equivalente a questo:Perché C++ std :: map :: operator [] non usa inplace new?

(*((std::map<>::insert(std::make_pair(x, T()))).first)).second 

Si chiama il costruttore di default del vostro tipo al fine di costruire quella coppia. Questo valore temporaneo viene quindi copiato nella mappa e quindi distrutto. La conferma di ciò può essere trovata in this stackoverflow post e here on codeguru.

Quello che trovo strano è che questo potrebbe essere implementato senza la necessità della variabile temporanea e comunque essere equivalente. C'è una funzionalità di C++ chiamata "inplace new". Std :: map e altri contenitori potrebbero allocare spazio vuoto per l'oggetto da vivere e quindi richiamare esplicitamente il costruttore predefinito dell'elemento nello spazio allocato.

La mia domanda: Perché nessuna delle implementazioni di std :: map che ho visto utilizzare inplace new per ottimizzare questa operazione? Mi sembra che migliorerebbe considerevolmente le prestazioni di questa operazione di basso livello. Ma molti occhi hanno studiato la base del codice STL, quindi immagino ci debba essere una ragione per cui viene fatto in questo modo.

+0

Il test case è incluso nel post di stackoverflow collegato. Ecco di nuovo il collegamento: https://stackoverflow.com/questions/4017892/in-an-stl-map-of-structs-why-does-the-operator-cause-the-struct-dtor-to – srm

+0

Mike Seymour: Quindi stai suggerendo che con il debugger disattivato, queste chiamate extra al distruttore sarebbero andate via? Sai che è vero? Anche se è vero, vuol dire che con il debugging, l'esecuzione è più difficile da eseguire il debug (non riesco a impostare un punto di interruzione nel mio debugger in cerca di problemi di ambito "reali"). È anche meno performante durante il debug, che è un problema minore, ma ancora reale. Entrambe queste ragioni mi sembrano motivi per utilizzare l'inplace new invece per una libreria di basso livello. – srm

risposta

4

In generale, si specifica una operazione di livello superiore come [] in termini di quelli di livello inferiore è una buona idea.

Prima di C++ 11, farlo con [] sarebbe difficile senza utilizzare insert.

In C++ 11, l'aggiunta di std::map<?>::emplace e materiale simile per std::pair ci dà la possibilità di evitare questo problema. Se lo hai ridefinito in modo tale da utilizzare una tale costruzione sul posto, la creazione di oggetti extra (auspicabilmente elidata) andrebbe via.

Non riesco a pensare a una ragione per non farlo. Ti incoraggio a proporlo per la standardizzazione.

Per dimostrare l'inserimento copia-meno in un std::map, siamo in grado di effettuare le seguenti operazioni:

#include <map> 
#include <iostream> 

struct no_copy_type { 
    no_copy_type(no_copy_type const&)=delete; 
    no_copy_type(double) {} 
    ~no_copy_type() { std::cout << "destroyed\n"; } 
}; 
int main() { 
    std::map< int, no_copy_type > m; 
    m.emplace(
    std::piecewise_construct, 
    std::forward_as_tuple(1), 
    std::forward_as_tuple(3.14) 
); 
    std::cout << "destroy happens next:\n"; 
} 

live example - come si può vedere, non temporanea viene generato.

Quindi, se sostituiamo

(*((std::map<>::insert(std::make_pair(x, T()))).first)).second 

con

(* 
    (
    (
     std::map<>::emplace(
     std::piecewise_construct, 
     std::forward_as_tuple(std::forward<X>(x)), 
     std::forward_as_tuple() 
    ) 
).first 
).second 

non temporanea si verrebbe a creare (spazi bianchi aggiunto in modo da poter tenere traccia di () s).

+0

La risposta precedente di Mike Seymour suggerisce che la capacità dei programmatori di scegliere un Allocatore alternativo significa che la costruzione del temporaneo non può essere rimossa dal codice e deve essere elidata durante l'ottimizzazione, il che è triste sia dal punto di vista del debug che dalla spiegazione a nuovi utenti perché un distruttore viene invocato durante l'inserimento. – srm

+0

@srm Sicuro. Ma Mike ha torto. È un difetto minore (inefficienza) dello standard, supponendo che non sia già stato risolto (non ho controllato l'ultima). Gli allocatori possono essere rimbalzati, e la costruzione e la costruzione a tratti possono consentirvi di costruire l'oggetto senza alcun intervento temporaneo. La costruzione predefinita può essere eseguita tramite una 'tupla 'vuota. – Yakk

+0

Non so abbastanza per commentare. Ecco perché ho fatto la domanda. Puoi pubblicare il tuo commento sulla risposta di Mike in modo da ricevere una notifica e vedere se voi due potete convincervi a vicenda? – srm

0

Prima di tutto, per operator[<key>]std::map equivale solo per un'operazione di inserimento se la richiesta <key> non viene trovato. In questo caso, è necessario solo un riferimento alla chiave e solo un riferimento al valore memorizzato prodotto.

In secondo luogo, quando viene inserito il nuovo elemento non è possibile sapere se verrà seguita un'operazione di copia. Potresti avere map[_k] = _v; o potresti avere _v = map[_k];.Quest'ultimo ha ovviamente gli stessi requisiti che esula da un incarico, ovvero map[_k].method_call();, ma lo non utilizza il costruttore di copie (non esiste una fonte da cui costruire). Per quanto riguarda l'inserimento, tutto quanto sopra richiede che venga chiamato il costruttore predefinito di value_type e che lo spazio venga assegnato per esso. Anche se potessimo sapere quando scriviamo operator[] che eravamo nel caso d'uso dell'assegnazione, non potremmo usare "inplace new" a causa dell'ordine delle operazioni. Il costruttore value_type dovrebbe essere chiamato per primo, seguito da value_type::operator=, che richiede la chiamata del costruttore di copie.

Bel pensiero però.