Come passare un oggetto polimorfico a un algoritmo STL

Come passare un oggetto polimorfico a un algoritmo STL

Come possiamo leggere nel capitolo di apertura del C++ efficace, il C++ è una federazione di 4 linguaggi:

  • la parte processuale proveniente da C,
  • la parte orientata agli oggetti,
  • la parte STL (seguendo un paradigma di programmazione funzionale),
  • la parte generica con i modelli.

E per di più, tutti questi 4 sottolinguaggi fanno parte di un tutto:il linguaggio C++. Quei 4 paradigmi che iniziano uniti in una lingua danno loro l'opportunità di interagire – e spesso queste interazioni creano situazioni interessanti.

Oggi ci concentriamo su una particolare interazione, tra il modello orientato agli oggetti e l'STL. Potrebbero esserci più forme per questa interazione e il caso che esamineremo è come passare un oggetto funzione polimorfico (ovvero con metodi virtuali) a un algoritmo STL.

Questo è un caso che mi è stato presentato e di cui vorrei condividere con voi la risoluzione. Come vedrai, questi due mondi non si integrano perfettamente tra loro, ma possiamo creare un ponte tra loro senza troppi sforzi.

Oggetti funzione polimorfici?

Per oggetto funzione , intendo un oggetto che ha un operator() . Può essere un lambda o un functor.

E polimorfico può significare molte cose in pratica, ma in questo contesto mi riferisco al polimorfismo di runtime con metodi virtuali .

Quindi il nostro oggetto funzione polimorfa può assomigliare a questo:

struct Base
{
    int operator()(int) const
    {
        method();
        return 42;
    }
    virtual void method() const { std::cout << "Base class called.\n"; }
};

In effetti, questo è un oggetto funzione totalmente debilitato che non fa nulla di significativo, ma che ci sarà utile per concentrare la nostra attenzione sull'effetto del passarlo a un algoritmo STL. Il caso originale aveva un dominio più ricco, ma non è il punto qui.

Ad ogni modo, tali oggetti polimorfici sono progettati per essere ereditati. Ecco un Derived classe che sovrascrive il metodo virtuale:

struct Derived : public Base
{
    void method() const override { std::cout << "Derived class called.\n"; }
};

Usiamo ora un Derived oggetto per invocare un algoritmo:

void f(Base const& base)
{
    std::vector<int> v = {1, 2, 3};
    std::transform(begin(v), end(v), begin(v), base);
}

int main()
{    
    Derived d;
    f(d);
}

Cosa pensi che produca questo codice?

Svela l'output di seguito per verificare se avevi ragione:

Base class called.
Base class called.
Base class called.

Non è sorprendente? Abbiamo superato un Derived oggetto all'algoritmo, ma l'algoritmo non chiama la funzione virtuale sovrascritta! Per capire cosa è successo, diamo un'occhiata al prototipo del std::transform algoritmo:

template< typename InputIterator, typename OutputIterator, typename Function>
OutputIt transform(InputIterator first, InputIterator last, OutputIterator out, Function f);

Osserva attentamente l'ultimo parametro (la funzione) e nota che è passato per valore .

Ma come spiegato nell'articolo 20 del C++ effettivo, gli oggetti polimorfici vengono tagliati quando li passiamo per valore:anche se Base const& riferimento base si riferiva a un Derived oggetto, facendo una copia di base crea un Base oggetto e non un Derived oggetto.

Quindi abbiamo bisogno di un modo per fare in modo che l'algoritmo utilizzi un riferimento all'oggetto polimorfico e non una copia.

Come lo facciamo?

Inserimento in un altro oggetto funzione

Questa è probabilmente l'idea che mi viene in mente per prima:un problema di informatica? Creiamo una indiretta!

Se il nostro oggetto deve essere passato per riferimento e l'algoritmo accetta solo copie, possiamo creare un oggetto intermedio che contenga un riferimento all'oggetto polimorfico e che possa essere esso stesso passato per copia.

Il modo più semplice per implementare questo oggetto funzione intermedia è con un lambda, che accetta base per riferimento:

std::transform(begin(v), end(v), begin(v), [&base](int n){ return base(n); }

Il codice ora restituisce:

Derived class called.
Derived class called.
Derived class called.

Funziona, ma ha lo svantaggio di appesantire il codice con una lambda esistente solo per scopi tecnici.

Nell'esempio sopra la lambda è piuttosto breve, ma potrebbe diventare ingombrante in un codice più simile alla produzione:

std::transform(begin(v), end(v), begin(v), [&base](module::domain::component myObject){ return base(myObject); }

Questo è un boccone di codice che non aggiunge alcun significato funzionale alla codeline.

Una soluzione compatta:utilizzando std::ref

C'è un altro modo per aggirare il problema del passaggio dell'oggetto polimorfico per valore, e consiste nell'usare std::ref :

std::transform(begin(v), end(v), begin(v), std::ref(base));

Ha lo stesso effetto della lambda. In effetti, il codice restituisce ancora:

Derived class called.
Derived class called.
Derived class called.

Ora c'è la possibilità che leggere questo ti abbia fatto andare così:

Certamente l'ha fatto a me.

Come diavolo potrebbe essere compilato questo codice in primo luogo? std::ref restituisce un std::reference_wrapper , che non è altro che un oggetto che modella un riferimento (tranne che puoi riassegnarlo per fare riferimento a un altro oggetto con il suo operator= ).

Come potrebbe svolgere il ruolo di oggetto funzione?

Ho scavato nella documentazione di std::reference_wrapper su cppreference.com e ho trovato questo:

Quindi questa è una caratteristica specifica inserita in std::reference_wrapper :quando std::ref prende un oggetto funzione F , l'oggetto restituito è anche un oggetto funzione che richiede un riferimento a F e offre un operator() che chiama F . Esattamente ciò di cui avevamo bisogno qui.

E noterai che per quanto grande o annidato nei namespace sia il tipo di tipo polimorfico, quello che passiamo agli algoritmi rimane std::ref(base) .

Una soluzione migliore?

Sembra che la soluzione utilizzi std::ref sostituisce quello che utilizza una lambda perché fa la stessa cosa ma con meno codice.

Ora potrebbero esserci altre soluzioni a questo problema, e anche migliori. Se vedi un altro modo per farlo, sarò felice di leggerlo nelle sezioni dei commenti appena sotto!

Articolo correlato:

  • Oggetti funzione STL:Stateless è Stressles