Domanda

Come regola generale, preferisco usare la semantica del valore piuttosto che quella del puntatore in C++ (cioè usare vector<Class> invece di vector<Class*>).Di solito la leggera perdita di prestazioni è più che compensata dal non doversi ricordare di eliminare gli oggetti allocati dinamicamente.

Sfortunatamente, le raccolte di valori non funzionano quando si desidera archiviare una varietà di tipi di oggetti che derivano tutti da una base comune.Vedi l'esempio qui sotto.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

La mia domanda è:Posso avere la mia torta (semantica del valore) e mangiarla anche (contenitori polimorfici)?Oppure devo usare i puntatori?

È stato utile?

Soluzione

Poiché gli oggetti di classi diverse avranno dimensioni diverse, finiresti per imbatterti nel problema dell'affettamento se li memorizzi come valori.

Una soluzione ragionevole è archiviare puntatori intelligenti sicuri per il contenitore.Normalmente utilizzo boost::shared_ptr che è sicuro da archiviare in un contenitore.Tieni presente che std::auto_ptr non lo è.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr utilizza il conteggio dei riferimenti quindi non eliminerà l'istanza sottostante finché non verranno rimossi tutti i riferimenti.

Altri suggerimenti

Si, puoi.

La libreria boost.ptr_container fornisce versioni semantiche a valore polimorfico dei contenitori standard.Devi solo passare un puntatore a un oggetto allocato nell'heap e il contenitore ne assumerà la proprietà e tutte le ulteriori operazioni forniranno la semantica del valore, ad eccezione del recupero della proprietà, che ti offre quasi tutti i vantaggi della semantica del valore utilizzando un puntatore intelligente .

Volevo solo sottolineare che vector<Foo> è solitamente più efficiente di vector<Foo*>.In un vettore<Foo>, tutti i Foo saranno adiacenti tra loro in memoria.Supponendo un TLB e una cache freddi, la prima lettura aggiungerà la pagina al TLB e inserirà una parte del vettore nelle cache L#;le letture successive utilizzeranno la cache calda e il TLB caricato, con occasionali errori di cache e errori TLB meno frequenti.

Confrontalo con un vettore<Foo*>:Man mano che riempi il vettore, ottieni Foo* dal tuo allocatore di memoria.Supponendo che il tuo allocatore non sia estremamente intelligente (tcmalloc?) o che riempi il vettore lentamente nel tempo, è probabile che la posizione di ciascun Foo sia molto distante dagli altri Foo:forse solo a centinaia di byte, forse megabyte di distanza.

Nel peggiore dei casi, mentre esegui la scansione di un vettore<Foo*> e dereferenzia ogni puntatore, incorrerai in un errore TLB e in un errore di cache: questo finirà per essere un quantità più lento che se avessi un vettore<Foo>.(Bene, nel peggiore dei casi, ogni Foo è stato paginato su disco e ogni lettura comporta una ricerca del disco () e una lettura () per spostare nuovamente la pagina nella RAM.)

Quindi, continua a utilizzare vector<Foo> quando appropriato.:-)

La maggior parte dei tipi di contenitori desidera astrarre la particolare strategia di archiviazione, che si tratti di elenco collegato, vettoriale, basato su albero o altro.Per questo motivo, avrai problemi sia a possedere che a consumare la torta di cui sopra (ovvero, la torta è bugia (NB:qualcuno doveva fare questa battuta)).

Quindi che si fa?Beh, ci sono alcune opzioni carine, ma la maggior parte si riduce a varianti su uno dei pochi temi o combinazioni di essi:scegliendo o inventando un puntatore intelligente adatto, giocando con modelli o modelli di modello in qualche modo intelligente, utilizzando un'interfaccia comune per i contenitori che fornisce un gancio per implementare il doppio invio per contenitore.

C'è una tensione di base tra i due obiettivi dichiarati, quindi dovresti decidere cosa vuoi, quindi provare a progettare qualcosa che ti dia sostanzialmente ciò che desideri.Esso È possibile fare alcuni trucchi carini e inaspettati per far sì che i puntatori assomiglino a valori con un conteggio dei riferimenti abbastanza intelligente e implementazioni abbastanza intelligenti di una fabbrica.L'idea di base è quella di utilizzare il conteggio dei riferimenti, la copia su richiesta e la costanza e (per il fattore) una combinazione di preprocessore, modelli e regole di inizializzazione statica di C++ per ottenere qualcosa che sia il più intelligente possibile nell'automazione delle conversioni dei puntatori.

In passato, ho passato un po' di tempo cercando di immaginare come utilizzare il proxy virtuale/lettera-busta/quel simpatico trucco con puntatori contati di riferimento per realizzare qualcosa di simile a una base per la programmazione semantica del valore in C++.

E penso che si potrebbe fare, ma dovresti fornire un mondo abbastanza chiuso, simile a codice gestito da C# all'interno di C++ (sebbene uno da cui potresti passare al C++ sottostante quando necessario).Quindi ho molta simpatia per la tua linea di pensiero.

Potresti anche considerare potenziamento::qualsiasi.L'ho usato per contenitori eterogenei.Quando si rilegge il valore, è necessario eseguire un any_cast.Se fallisce, lancerà un bad_any_cast.Se ciò accade, puoi catturarlo e passare al tipo successivo.

IO credere lancerà un bad_any_cast se provi a any_cast una classe derivata alla sua base.L'ho provato:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

Detto questo, seguirei prima anche il percorso shared_ptr!Ho solo pensato che potesse interessarti.

Dare un'occhiata a static_cast E reinterpret_cast
In C++ Programming Language, 3a edizione, Bjarne Stroustrup lo descrive a pagina 130.C'è un'intera sezione su questo nel capitolo 6.
Puoi ritrasformare la tua classe Genitore in classe Figlio.Ciò richiede che tu sappia quando ciascuno è quale.Nel libro, il Dott.Stroustrup parla di diverse tecniche per evitare questa situazione.

Non farlo.Ciò annulla il polimorfismo che stai cercando di ottenere in primo luogo!

Giusto per aggiungere una cosa a tutte 1800 INFORMAZIONI già detto.

Potresti voler dare un'occhiata "C++ più efficace" di Scott Mayers "Elemento 3:Non trattare mai gli array in modo polimorfico" per comprendere meglio questo problema.

Sto utilizzando la mia classe di raccolta basata su modelli con semantica del tipo di valore esposto, ma internamente memorizza i puntatori.Utilizza una classe iteratore personalizzata che, quando dereferenziata, ottiene un riferimento al valore anziché un puntatore.La copia della raccolta crea copie profonde degli elementi, anziché puntatori duplicati, ed è qui che si trova la maggior parte delle spese generali (un problema davvero minore, considerato ciò che ottengo invece).

E' un'idea che potrebbe soddisfare le tue esigenze.

Durante la ricerca di una risposta a questo problema, mi sono imbattuto sia in questo che in una domanda simile.Nelle risposte all'altra domanda troverai due soluzioni suggerite:

  1. Utilizza std::optional o boost::optional e un pattern visitatore.Questa soluzione rende difficile aggiungere nuovi tipi, ma è facile aggiungere nuove funzionalità.
  2. Utilizza una classe wrapper simile a what Sean Parent presenta nel suo discorso.Questa soluzione rende difficile aggiungere nuove funzionalità, ma è facile aggiungere nuovi tipi.

Il wrapper definisce l'interfaccia necessaria per le tue classi e contiene un puntatore a uno di questi oggetti.L'implementazione dell'interfaccia avviene con funzioni gratuite.

Ecco un esempio di implementazione di questo modello:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

Forme diverse vengono quindi implementate come strutture/classi regolari.Sei libero di scegliere se desideri utilizzare le funzioni membro o le funzioni libere (ma dovrai aggiornare l'implementazione di cui sopra per utilizzare le funzioni membro).Preferisco le funzioni gratuite:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

Ora puoi aggiungerli entrambi Circle E Rectangle si oppone allo stesso std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

Lo svantaggio di questo modello è che richiede una grande quantità di boilerplate nell'interfaccia, poiché ciascuna funzione deve essere definita tre volte.Il lato positivo è che ottieni la semantica della copia:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

Questo produce:

Drew rectangle with width 2
Drew rectangle with width 2

Se sei preoccupato per il shared_ptr, puoi sostituirlo con a unique_ptr.Tuttavia, non sarà più copiabile e dovrai spostare tutti gli oggetti o implementare la copia manualmente.Sean Parent ne discute in dettaglio nel suo discorso e un'implementazione è mostrata nella risposta sopra menzionata.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top