Devo passare una funzione std :: tramite const-reference?


141

Diciamo che ho una funzione che accetta un std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Dovrei invece passare xper const-reference ?:

void callFunction(const std::function<void()>& x)
{
    x();
}

La risposta a questa domanda cambia a seconda di ciò che la funzione fa con essa? Ad esempio, se è una funzione o un costruttore di un membro di classe che archivia o inizializza la std::functionvariabile in un membro.


1
Probabilmente no. Non lo so per certo, ma mi aspetterei sizeof(std::function)di non essere altro 2 * sizeof(size_t), che è la dimensione minima che avresti mai considerato per un riferimento const.
Mats Petersson,

12
@Mats: non credo che la dimensione del std::functionwrapper sia importante quanto la complessità della sua copia. Se sono coinvolte copie profonde, potrebbe essere molto più costoso di quanto sizeofsuggerisce.
Ben Voigt,

Dovresti avere movela funzione?
Yakk - Adam Nevraumont,

operator()()è constquindi un riferimento const dovrebbe funzionare. Ma non ho mai usato la funzione std ::.
Neel Basu,

@Yakk Ho appena passato un lambda direttamente alla funzione.
Sven Adbring,

Risposte:


79

Se desideri prestazioni, passa per valore se le stai memorizzando.

Supponiamo di avere una funzione chiamata "esegui questo nel thread dell'interfaccia utente".

std::future<void> run_in_ui_thread( std::function<void()> )

che esegue del codice nel thread "ui", quindi segnala il completamento future. (Utile nei framework dell'interfaccia utente in cui il thread dell'interfaccia utente è il punto in cui dovresti pasticciare con gli elementi dell'interfaccia utente)

Abbiamo due firme che stiamo prendendo in considerazione:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Ora, è probabile che utilizzeremo questi come segue:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

che creerà una chiusura anonima (un lambda), ne costruirà uno std::functionfuori, lo passerà alla run_in_ui_threadfunzione, quindi attenderà che finisca l'esecuzione nel thread principale.

Nel caso (A), il std::functionè direttamente costruito dal nostro lambda, che viene quindi utilizzato all'interno del file run_in_ui_thread. Il lambda è moved in std::function, quindi ogni stato mobile viene efficacemente trasportato in esso.

Nel secondo caso, std::functionviene creato un temporaneo , il lambda è moved in esso, quindi quel temporaneo std::functionviene utilizzato come riferimento all'interno di run_in_ui_thread.

Fin qui tutto bene - i due si esibiscono in modo identico. Tranne che run_in_ui_threadsta per fare una copia del suo argomento di funzione da inviare al thread dell'interfaccia utente per l'esecuzione! (tornerà prima di averlo fatto, quindi non può semplicemente usare un riferimento ad esso). Per il caso (A), ci limitiamo semplicemente movealla std::functionsua conservazione a lungo termine. Nel caso (B), siamo costretti a copiare il file std::function.

Quel negozio rende il passaggio per valore più ottimale. Se è possibile che tu stia memorizzando una copia di std::function, passa per valore. Altrimenti, in entrambi i casi è approssimativamente equivalente: l'unico aspetto negativo del per valore è se stai prendendo lo stesso ingombrante std::functione hai un metodo secondario dopo l'altro usarlo. A parte questo, a movesarà efficiente quanto a const&.

Ora, ci sono alcune altre differenze tra i due che entrano principalmente se abbiamo uno stato persistente all'interno di std::function.

Supponiamo che std::functionmemorizzi un oggetto con a operator() const, ma ha anche alcuni mutablemembri di dati che modifica (che maleducato!).

Nel std::function<> const&caso, i mutablemembri dei dati modificati si propagheranno fuori dalla chiamata di funzione. Nel std::function<>caso, non lo faranno.

Questo è un caso angolare relativamente strano.

Vuoi trattare std::functioncome qualsiasi altro tipo possibilmente pesante, a buon mercato mobile. Lo spostamento è economico, la copia può essere costosa.


Il vantaggio semantico di "passa per valore se lo stai memorizzando", come dici tu, è che per contratto la funzione non può mantenere l'indirizzo dell'argomento passato. Ma è proprio vero che "A parte questo, una mossa sarà efficiente come una const &"? Vedo sempre il costo di un'operazione di copia più il costo dell'operazione di spostamento. Con il passaggio const&vedo solo il costo dell'operazione di copia.
ceztko,

2
@ceztko In entrambi i casi (A) e (B), il temporaneo std::functionviene creato dalla lambda. In (A), il temporaneo viene eluso nell'argomento run_in_ui_thread. In (B) viene passato un riferimento a detto temporaneo run_in_ui_thread. Finché i tuoi std::functions sono creati da lambdas come temporanei, la clausola vale. Il paragrafo precedente tratta il caso in cui std::functionpersiste. Se non stiamo memorizzando, stiamo semplicemente creando da un lambda function const&e functionabbiamo lo stesso overhead esatto.
Yakk - Adam Nevraumont,

Ah, capisco! Questo ovviamente dipende da ciò che accade al di fuori di run_in_ui_thread(). C'è solo una firma per dire "Passa per riferimento, ma non memorizzerò l'indirizzo"?
ceztko,

@ceztko No, non c'è.
Yakk - Adam Nevraumont,

1
@ Yakk-AdamNevraumont se fosse più completo per coprire un'altra opzione per passare per riferimento valore:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P

33

Se sei preoccupato per le prestazioni e non stai definendo una funzione membro virtuale, molto probabilmente non dovresti usare std::functionaffatto.

Rendere il tipo di funzione un parametro modello consente un'ottimizzazione maggiore rispetto std::function, incluso l'inserimento della logica del funzione. È probabile che l'effetto di queste ottimizzazioni superi di gran lunga le preoccupazioni di copia contro indiretta su come passare std::function.

Più veloce:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

1
In realtà non sono affatto preoccupato per le prestazioni. Ho solo pensato che usare riferimenti costuali dove dovrebbero essere usati è una pratica comune (mi vengono in mente stringhe e vettori).
Sven Adbring,

13
@Ben: Penso che il modo più moderno di implementare questo hippy sia quello di utilizzare questo std::forward<Functor>(x)();, per preservare la categoria di valore del funzione, poiché è un riferimento "universale". Tuttavia, non farà la differenza nel 99% dei casi.
GManNickG,

1
@Ben Voigt, quindi per il tuo caso, chiamerei la funzione con una mossa? callFunction(std::move(myFunctor));
arias_JC

2
@arias_JC: se il parametro è un lambda, è già un valore. Se si dispone di un valore, è possibile utilizzare std::movese non sarà più necessario in altro modo o passare direttamente se non si desidera uscire dall'oggetto esistente. Le regole di collasso dei riferimenti assicurano che callFunction<T&>()abbia un parametro di tipo T&, no T&&.
Ben Voigt,

1
@BoltzmannBrain: ho scelto di non apportare tale modifica perché è valida solo per il caso più semplice, quando la funzione viene chiamata una sola volta. La mia risposta è alla domanda "come devo passare un oggetto funzione?" e non limitato a una funzione che non fa nulla oltre a invocare incondizionatamente quel funzione esattamente una volta.
Ben Voigt,

25

Come al solito in C ++ 11, passare per valore / riferimento / const-riferimento dipende da cosa fai con il tuo argomento. std::functionnon è diverso.

Il passaggio per valore consente di spostare l'argomento in una variabile (in genere una variabile membro di una classe):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Quando sai che la tua funzione sposterà il suo argomento, questa è la soluzione migliore, in questo modo i tuoi utenti possono controllare come chiamano la tua funzione:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Credo che tu conosca già la semantica dei (non) riferimenti costanti, quindi non voglio chiarire il punto. Se hai bisogno che io aggiunga ulteriori spiegazioni al riguardo, basta chiedere e aggiornerò.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.