2016-06-11 69 views
13

Supponiamo di avere uno Storage di alcuni Object s con un metodo che aggrega i puntatori ad alcuni degli Objects in un vettore. Come questo:C'è un modo per convertire std :: vector <const T*> in std :: vector <T*> senza allocazioni aggiuntive?

class Storage 
{ 
public: 
    std::vector<Object*> aggregate_some_objects(); // non-const version 
    std::vector<const Object*> aggregate_some_objects() const; // const version 

private: 
    std::unordered_map<size_t, Object> m_objects; // data is stored 
               // by-value in a non-vector container 
} 

In generale, non c'è modo per evitare di copia-incolla in attuazione coppie metodo non-const const + chiamando uno di loro dentro l'altro con l'aiuto di const_cast. Qui tuttavia questo non è possibile perché i tipi di ritorno dei metodi sono diversi.

Il modo più semplice per evitare il copia-incolla qui sarebbe chiamare const versione da una versione non const e utilizzare il tornata std::vector<const T*> per popolare un separato std::vector<T*>. Tuttavia, ciò comporterebbe almeno 2 allocazioni di heap (una per ciascun vettore). Vorrei evitare le allocazioni associate al secondo vettore.

mi chiedo se non v'è modo di scrivere qualcosa di simile

template <typename T> 
std::vector<T*> remove_const_from_vector_of_ptrs(std::vector<const T*>&& input) 
{ 
    std::vector<T*> ret; 
    // do some magic stuff here that does not involve 
    // more memory allocations 
    return ret; 
} 

Così, permettendo di scrivere

std::vector<const Object*> Storage::aggregate_some_objects() const 
{ 
    // non-trivial implementation 
} 

std::vector<Object*> Storage::aggregate_some_objects() 
{ 
    auto objects = const_cast<const Storage*>(this)->aggregate_some_objects(); 
    return remove_const_from_vector_of_ptrs(std::move(objects)); 
} 

Non esiste un metodo 'rilascio' in std::vector (come std::unique_ptr per esempio) che permette trasferimento della proprietà della memoria - e per una buona ragione, quindi mi aspetto che ciò non sia possibile.

Capisco anche che se fosse possibile, sarebbe un'operazione pericolosa che dovrebbe essere generalmente evitata, proprio come const_cast. Ma un uso attento in casi come questo sembra più vantaggioso del copia-incolla.

Edit: aggiunto chiarimenti a ciò che intendo per allocazioni 'extra' e cambiato Storage::aggregate_objects()-Storage::aggregate_some_objects() per meglio indicare che l'attuazione di questi metodi è più complessa poi un ciclo di gamma basata - da qui il desiderio di evitare la copia -spedizione dell'attuazione.

+0

Le funzioni restituiscono per valore, quindi allocare un nuovo vettore ogni volta. Quali "allocazioni extra" stai cercando di evitare? –

+0

Sì, le funzioni restituiscono in base al valore, il che significa che deve verificarsi almeno un'allocazione dell'heap (caso migliore: conosco la dimensione del vettore restituito e posso chiamare 'vector :: reserve'). Sto parlando di allocazioni che si svolgeranno nella più semplice implementazione dei metodi in questione che non coinvolge il copia-incolla: la versione non-'const' chiama la versione 'const' (che restituisce' std :: vector ') e popola un separato 'std :: vector '. La creazione di un vettore separato comporterà almeno un'altra assegnazione ("extra") che vorrei evitare. Chiarito questo nella domanda. –

+1

Se l'obiettivo è semplicemente di evitare la duplicazione del corpo della funzione, allora non provare a fare qualcosa di stupido con calchi e comportamento indefinito, basta scrivere un modello di funzione che restituisce 'std :: vector ' e quindi invocarlo con 'T' = = 'Oggetto' o con' T' == 'const Object'. –

risposta

13

La risposta breve è: no. std::vector<Object*> e std::vector<const Object*> sono due diverse classi indipendenti. Sono diversi l'uno dall'altro come class A è da class B. Spesso si pensa che solo perché entrambi iniziano con std::vector, che sono in qualche modo correlati tra loro. Questo non è vero, e per questo motivo non c'è modo di convertire l'uno con l'altro, "sul posto". Ognuna di queste classi di vettori possiede il loro corrispondente interno data() e non cederà volentieri ad altre classi strane.

La risposta lunga è ancora no, ma in molti casi è possibile aggirare il problema al fine di evitare la duplicazione del codice manuale. La verità è che la duplicazione del codice è inevitabile nella maggior parte di questi tipi di situazioni e il meglio che si può fare è evitare la duplicazione manuale del codice.

Un approccio comune è avere sia la costante e il metodo della classe mutabile essere facciate di un singolo, condiviso, modello privato:

// Header file: 

class Storage { 

public: 

    std::vector<const Object*> aggregate_objects() const; 

    std::vector<Object*> aggregate_objects(); 

private: 

    template<typename v_type> void make_aggregate_objects(v_type &v) const; 
}; 

// In the translation unit: 

template<typename v_type> void Storage::make_aggregate_objects(v_type &v) const 
{ 
    // Now, create 'v' here... v.reserve(), v.push_back(), etc... 
} 

std::vector<const Object*> Storage::aggregate_objects() const 
{ 
    std::vector<const Object *> v; 

    make_aggregate_objects(v); 

    return v; 
} 

std::vector<Object*> Storage::aggregate_objects() 
{ 
    std::vector<const Object *> v; 

    make_aggregate_objects(v); 

    return v; 
} 

Il compilatore sarà ancora generare due pezzi quasi identiche di codice, ma almeno non stai facendo tutta la digitazione.

Un altro approccio simile consiste nel passare un lambda alla funzione template invece di passare un oggetto vettoriale, con la funzione di modello privato che utilizza la funzione lambda come callback per costruire il vettore restituito. Con un po 'di cancellazione del tipo e un po' di aiuto da std::function, il metodo della classe privata può essere trasformato in un metodo ordinario, anziché in un metodo di modello.

+0

Questa sembra la risposta più completa con la soluzione più semplice. Grazie! Voglio solo aggiungere una cosa: sebbene tu chiami questo approccio comune, voglio sottolineare che se dovessi incontrare un codice del genere probabilmente mi gratterò la testa per un secondo o due pensando perché il 'v_type' è templatizzato. È inteso per essere usato con vettori diversi (o per entità completamente diverse se l'argomento di 'make_aggregate_objects' era' std :: vector & v')? Penso che l'approccio 'std :: conditional_t' proposto da ** tmlen ** aggiunga chiarezza a tali equivoci, forse alcune persone lo troveranno più utile. –

+0

@SergeyNikitin: il vantaggio principale dell'uso di 'std :: conditional_t' è di forzare un parametro errato a generare un errore di compilazione in precedenza nel processo, al contrario di un errore di compilazione nascosto da qualche parte all'interno della funzione e fare riferimento ad alcuni interni, non documentati membri di contenitori di librerie C++ che non hanno apparentemente a che fare con nulla. Immagino che dopo ~ 15 anni di decifrazione degli errori di compilazione C++, questo è per me un valore molto scarso; e ottengo la possibilità di riutilizzare il modello con altri contenitori, gratuitamente. –

0

La risposta ovvia è no, std::vector<T*> e std::vector<const T*> sono 2 oggetti diversi che non possono essere interscambiati.

Tuttavia, armati con la conoscenza di std::vector interni, dovrebbe essere abbastanza facile vedere che se noi conserviamo T* o const T* all'interno std::vector, non importa dal suo punto di vista.

Quindi, un modo per farlo sarebbe quello di eseguire il cast forzato del risultato da un tipo all'altro. Quindi:

template <typename T> 
std::vector<T*> remove_const_from_result(std::vector<const T*>&& input) { 
    return reinterpret_cast<std::vector<T*>&>(input); 
} 

Il grande avvertimento con questo approccio è che possiamo fare solo se siamo abbastanza fiduciosi riguardo la struttura interna del contenitore siamo forza-casting.

Si noti che questo ancora non aiuta con il tuo problema originale, vale a dire che hai 2 funzioni membro diverse, e sei essenzialmente const_cast -ing il risultato di uno in un altro.

Per illustrare, lasciate che suppone che si abbia una funzione getter senza la std::vector, in modo da prendere questo:

struct Foo { 
    const Bar* bar() const { return &bar_; } 
    Bar* bar() { return const_cast<Bar*>(bar()); } 

private: 
    Bar bar_; 
}; 

Un modo migliore per fare questo, IMO, sarebbe quello di esporre una funzione templatized internamente, e l'uso quello invece. Così, Foo diventa:

struct Foo { 
    const Bar* bar() const { return get_internal<const Bar*>(&bar_); } 
    Bar* bar() { return get_internal<Bar*>(&bar_); } 

private: 
    Bar bar_; 
    template <typename T> 
    T get_internal(std::remove_const<T>::type ptr) const { return ptr; } 
}; 

Così, allo stesso modo, per il tuo esempio, è possibile utilizzare uno stesso approccio:

struct Storage { 
    std::vector<const Object*> aggregate_objects() const { return aggregate_internal<const Object*>(); } 
    std::vector<Object*> aggregate_objects() { return aggregate_internal<Object*>(); } 

private: 
    template <typename T> 
    std::vector<T*> aggregate_internal() const { 
    // actual aggregate function where T* can be const T* also. 
    } 
} 
+2

L'approccio "forza cast" non viene compilato e non evita allocazioni aggiuntive anche se è stato compilato perché restituito in base al valore (come l'originale). –

+0

Nell'esempio "force cast" si legge: 'modello std :: vector & remove_const_from_result (std :: vector e input) {return reinterpret_cast &> (input); } ' – Quuxplusone

+0

@JonathanWakely RVO dovrebbe garantire che non venga eseguita alcuna allocazione aggiuntiva. – Arindam

2

Non v'è alcun modo per convertire std::vector<const Object*>-std::vector<Object*> senza riallocando la memoria e la copia dei puntatori , perché std::vector è un contenitore e possiede la sua memoria.

Uso reinterpret_cast può funzionare in questo caso, ma è un comportamento indefinito e dipende dall'implementazione di std::vector:

std::vector<const Object*> const_vec = ...; 
std::vector<Object*>& vec = reinterpret_cast<std::vector<Object*>&>(const_vec); 

Una soluzione per evitare const_cast o inutili allocazioni sarebbe una terza funzione templated:

template<typename Stor> 
static auto Storage::aggregate_objects_(Stor&) 
-> std::vector<std::conditional_t<std::is_const<Stor>::value, const Object*, Object*>> 
{ 
    ... 
} 

dove Stor può essere Storage o const Storage.

Poi aggregate_objects() sarebbe essere implementata come:

std::vector<const Object*> Storage::aggregate_objects() const { 
    return aggregate_objects_(*this); 
} 

std::vector<Object*> Storage::aggregate_objects() { 
    return aggregate_objects_(*this); 
} 
1

Le funzioni restituiscono per valore in modo sempre allocare in ogni caso - ciò che "gli stanziamenti aggiuntivi" stai parlando?

Se semplicemente memorizzare vector<Object*> internamente allora è banale per risolvere il problema:

std::vector<Object*> Storage::aggregate_objects() 
{ return m_data; }; 

std::vector<const Object*> Storage::aggregate_objects() const 
{ return std::vector<const Object*>(m_data.begin(), m_data.end()); } 

Edit: in risposta alla tua domanda aggiornato:

non si deve scrivere codice cattivo solo per evitare la copia & incolla di un corpo di una funzione!

Non c'è bisogno di duplicare il corpo della funzione, o di scrivere codice cattivo con cast pericolosi o rischiosi, basta usare un modello che viene chiamato da entrambe le funzioni, come mostra la risposta di Sam Varshavchik.

+0

Ho modificato la domanda originale per chiarire cosa intendo per allocazioni "extra". Inoltre ho espanso frammenti di codice per dare un'idea migliore di come sono archiviati i dati, che si spera possano dare una migliore idea di quale problema si sta risolvendo. –

Problemi correlati