Come sottolinea @ JDługosz nei commenti, Herb fornisce altri consigli in un altro (più tardi?) Discorso, vedi più o meno da qui: https://youtu.be/xnqTKD8uD64?t=54m50s .
Il suo consiglio si riduce all'uso dei parametri di valore solo per una funzione f
che accetta i cosiddetti argomenti sink, supponendo che si sposterà il costrutto da questi argomenti sink.
Questo approccio generale aggiunge solo il sovraccarico di un costruttore di spostamenti per gli argomenti sia lvalue che rvalue rispetto a un'implementazione ottimale degli f
argomenti su misura rispettivamente lvalue e rvalue. Per capire perché questo è il caso, supponiamo che f
prenda un parametro value, dove T
è un tipo di copia e sposta costruibile:
void f(T x) {
T y{std::move(x)};
}
La chiamata f
con un argomento lvalue comporterà il richiamo di un costruttore di copie x
e di un costruttore di spostamenti y
. D'altra parte, la chiamata f
con un argomento rvalue farà sì che un costruttore di mosse venga chiamato per costruire x
e un altro costruttore di mosse venga chiamato per costruire y
.
In generale, l'implementazione ottimale degli f
argomenti for lvalue è la seguente:
void f(const T& x) {
T y{x};
}
In questo caso, solo un costruttore di copie viene chiamato per costruire y
. L'implementazione ottimale degli f
argomenti for rvalue è, sempre in generale, come segue:
void f(T&& x) {
T y{std::move(x)};
}
In questo caso, solo un costruttore di mosse viene chiamato per costruire y
.
Quindi un ragionevole compromesso è prendere un parametro value e avere una chiamata in più per il costruttore di mosse per argomenti lvalue o rvalue rispetto all'implementazione ottimale, che è anche il consiglio dato nel discorso di Herb.
Come ha sottolineato @ JDługosz nei commenti, passare per valore ha senso solo per le funzioni che costruiranno un oggetto dall'argomento sink. Quando hai una funzione f
che copia il suo argomento, l'approccio pass-by-value avrà un overhead maggiore di un approccio pass-by-const-reference generale. L'approccio pass-by-value per una funzione f
che conserva una copia del suo parametro avrà la forma:
void f(T x) {
T y{...};
...
y = std::move(x);
}
In questo caso, esiste una costruzione di copia e un'assegnazione di spostamento per un argomento lvalue, nonché una costruzione di spostamento e un'assegnazione di spostamento per un argomento rvalue. Il caso più ottimale per un argomento lvalue è:
void f(const T& x) {
T y{...};
...
y = x;
}
Questo si riduce a un solo compito, che è potenzialmente molto più economico del costruttore di copie più l'assegnazione di spostamento richiesta per l'approccio pass-by-value. La ragione di ciò è che l'assegnazione potrebbe riutilizzare la memoria allocata esistente y
e quindi impedire (de) allocazioni, mentre il costruttore della copia di solito allocerà la memoria.
Per un argomento rvalue l'implementazione più ottimale per f
quella conserva una copia ha la forma:
void f(T&& x) {
T y{...};
...
y = std::move(x);
}
Quindi, solo un'assegnazione di mosse in questo caso. Passare un valore alla versione di f
quello richiede un riferimento const costa solo un compito invece di un compito di spostamento. Quindi relativamente parlando, la versione di f
prendere un riferimento const in questo caso è preferibile l'implementazione generale.
Quindi, in generale, per l'implementazione più ottimale, dovrai sovraccaricare o fare una sorta di inoltro perfetto, come mostrato nel discorso. Lo svantaggio è un'esplosione combinatoria del numero di sovraccarichi richiesti, a seconda del numero di parametri f
nel caso in cui si scelga di sovraccaricare la categoria di valore dell'argomento. L'inoltro perfetto ha lo svantaggio che f
diventa una funzione modello, che impedisce di renderlo virtuale, e si traduce in un codice significativamente più complesso se si desidera farlo correttamente al 100% (vedere la discussione per i dettagli cruenti).