5
Riferimenti rvalue, semantica di spostamento e perfect-forward
Da principio, la semantica dello spostamento e il perfect-forward sembrano concetti piuttosto semplici.
• La semantica
di spostamento consente ai compilatori di sostituire delle
costose operazioni di copia con meno costosi spostamenti. Nello
stesso modo in cui i costruttori per copia e gli operatori di
assegnamento per copia offrono il controllo sulla copia degli
oggetti, i costruttori per spostamento e gli operatori
d’assegnamento per spostamento offrono il controllo sulla semantica
dello spostamento. La semantica della spostamento consente inoltre
di creare tipi move-only, come std::unique_ptr
, std::future
e std::thread
.
• Il perfect-forward consente di scrivere modelli di funzioni (template) che accettano argomenti arbitrari e li inoltrano ad altre funzioni, in modo che le funzioni di destinazione ricevano esattamente gli stessi argomenti passati alle funzioni che eseguono l’inoltro.
I riferimenti rvalue sono il collante che unisce queste due funzionalità. Sono il meccanismo offerto dal linguaggio per rendere possibile sia la semantica dello spostamento sia il perfect-forward.
Ma più esperienza si acquisisce su queste
funzionalità, più si comprende che l’impressione iniziale si basava
solo sulla proverbiale “punta dell’iceberg”. Il mondo della
semantica di spostamento, del perfect-forward e dei riferimenti
rvalue è più intricato di quanto sembri. Per esempio, std::move
non sposta nulla, e il perfect-forward
è tutt’altro che “perfetto”. Le operazioni di spostamento non
sempre sono più economiche rispetto a quelle di copia; quando lo
sono, non sono tanto economiche quanto si potrebbe immaginare; e
non sempre vengono richiamate in un contesto in cui lo spostamento
è valido. Il costrutto “type&&” non sempre rappresenta un
riferimento rvalue.
Indipendentemente da quanto si approfondiscano
queste funzionalità, c’è sempre l’impressione che ci sia ancora
qualcosa da scoprire. Fortunatamente, esiste un limite a questa
profondità. Questo capitolo vi accompagnerà proprio fin sul fondo.
Una volta che vi arriverete, questo aspetto del C++11 vi sarà molto
più chiaro. Per esempio, conoscerete le convenzioni d’uso per
std::move
e std::forward
. Vi troverete a vostro agio con la
natura ambigua di “type&&”. Comprenderete i motivi,
talvolta sorprendenti, degli strani comportamenti delle operazioni
di spostamento. Tutti questi dettagli avranno una loro precisa
collocazione. A quel punto, in modo paradossale, vi ritroverete
dove siete partiti, poiché la semantica dello spostamento, il
perfect-forward e i riferimenti rvalue torneranno a sembrarvi
concetti semplici. Ma, a quel punto, li avrete davvero compresi
appieno.
Negli Elementi di questo Capitolo, è particolarmente importante tenere in considerazione che un parametro è sempre un lvalue, anche se il suo tipo è un riferimento rvalue. Detto questo, dato
void f(Widget&& w);
il parametro w
è
un lvalue, anche se il suo tipo è un riferimento rvalue a
Widget
(se lo trovate sorprendente,
rileggete la panoramica su lvalues
e
rvalues
presente nell’Introduzione,
all’interno del paragrafo Terminologia e convenzioni).
Elemento 23 – Parliamo di std::move e std::forward
È utile iniziare a parlare di std::move
e std::forward
in termini di ciò che non fanno. std::move
non sposta niente. std::forward
non
inoltra niente. In fase di esecuzione del programma, nessuno dei
due fa nulla. Non generano codice eseguibile, neppure un unico
byte.
std::move
e
std::forward
sono semplicemente
funzioni (in realtà template di funzioni) che eseguono conversioni.
std::move
converte
incondizionatamente il proprio argomento in un rvalue, mentre
std::forward svolge questa conversione solo se è esaudita una
determinata condizione. Questo è tutto. Naturalmente una
descrizione così sintetica fa sorgere nuove domande, ma, in buona
sostanza, questo è tutto.
Per rendere più concreto il tutto, ecco un esempio di implementazione di std::move in C++11. Non è pienamente conforme ai dettagli dello Standard, ma gli si avvicina molto.
template<typename
T> |
// nel namespace
std |
typename
remove_reference<T>::type&& |
|
move(T&& param) |
|
{ |
|
using ReturnType
= |
// dichiarazione
alias; |
typename
remove_reference<T>::type&&; |
// vedi Elemento
9 |
return static_cast<ReturnType>(param); |
|
} |
Ho evidenziato due parti di codice. Un oè il nome della funzione, poiché la specifica del tipo restituito è piuttosto “rumorosa” e non voglio che in questo rumore possiate perdere dei dettagli. L’altra è la conversione che costituisce l’essenza della funzione. Come potete vedere, std::move prende un riferimento a un oggetto (un riferimento universale, per essere precisi, vedi Elemento 24) e restituisce un riferimento allo stesso oggetto.
La parte &&
del tipo restituito dalla funzione
implica che std::move
restituisce un
riferimento rvalue, ma, come spiega l’Elemento 28,
se il tipo T
è un riferimento lvalue,
T&&
diverrebbe un riferimento
lvalue. Per impedire ciò, a T
viene
applicato il tipe trait (vedi Elemento 9)
std::remove_reference
, garantendo
così che && venga applicato a un tipo che non sia un
riferimento. Ciò garantisce che std::move
restituisca davvero un riferimento
rvalue, e questo è importante, poiché i riferimenti rvalue
restituiti dalle funzioni sono rvalue. Pertanto, std::move
converte il proprio argomento in un
rvalue, e questo è tutto ciò che fa.
A proposito, std::move può essere implementata
con molta più facilità in C++14. Grazie alla deduzione del tipo
restituito dalle funzioni (vedi Elemento 3) e al template alias std::remove_reference_t
della Libreria Standard
(vedi Elemento 9), std::move può essere scritta in questo
modo:
template<typename
T> |
// C++14; sempre
nel |
decltype(auto) move(T&& param) |
// namespace
std |
{ |
|
using ReturnType =
remove_reference_t<T>&&; |
|
return
static_cast<ReturnType>(param); |
|
} |
Molto più leggibile, vero?
Poiché std::move non fa altro che convertire
il proprio argomento in un rvalue, qualcuno ha suggerito che un
nome migliore per questa funzione sarebbe stato qualcosa come
rvalue_cast
. Sia come sia, il nome
utilizzato è std::move, pertanto è importante ricordare ciò che
std::move
fa e non fa. Esegue una
conversione, non uno spostamento.
Naturalmente, gli rvalue sono candidati per
gli spostamenti, dunque l’applicazione di std::move
a un oggetto comunica al compilatore
che l’oggetto può essere spostato. Questo è il motivo per cui
std::move
ha questo nome: per
designare gli oggetti che possono essere spostati.
In verità, gli rvalue “solitamente” sono
candidati per lo spostamento. Supponete di dover scrivere una
classe che rappresenta generiche annotazioni. Il costruttore della
classe prende un parametro std::string
che costituisce l’annotazione e copia
il parametro in un dato membro. Come descritto nell’Elemento 41, si dichiara
un parametro per valore:
class Annotation
{ |
|
public: |
|
explicit
Annotation(std::string text); |
// parametro da
copiare, |
… |
// vedi Elemento 41, |
}; |
// passaggio per
valore |
Ma il
costruttore di Annotation
ha bisogno
solo di leggere il valore di text
.
Non deve modificarlo. Seguendo la lunga tradizione di utilizzare
const
quando possibile, si modifica
la dichiarazione in modo che text
sia
const
:
class Annotation
{
public:
explicit
Annotation(const std::string
text)
…
};
Per evitare di pagare per un’operazione di
copia quando si deve copiare text
in
un dato membro, ci atteniamo al consiglio fornito nel Punto 41 e
applichiamo std::move
a text
, producendo pertanto un rvalue:
class Annotation
{ |
|
public: |
|
explicit
Annotation(const std::string text) |
|
: value(std::move(text)) |
// “move” di text in
value; questo codice |
{ ... } |
// non fa quello che sembra! |
… |
|
private: |
|
std::string
value; |
|
}; |
Questo codice passa la compilazione. Passa
anche il linking. Viene anche seguito. Questo codice assegna al
dato membro value
il contenuto di
text
. L’unica cosa che separa questo
codice da una realizzazione perfetta di questa idea è che
text
non viene spostato in
value
, ma viene copiato. Certamente, text
viene convertito in un rvalue da
std::move
, ma text
è dichiarato come una const std::string
, pertanto prima della
conversione, text
è un lvalue const
std::string
e il risultato della
conversione è un rvalue const std::string
, ma, attraverso tutto questo, rimane
il fatto che text
è const
.
Considerate l’effetto che si ha quando i
compilatori devono determinare quale costruttore di std::string
richiamare. Hanno due
possibilità:
class string
{ |
// std::string è in
effetti un |
public: |
// typedef per
std::basic string<char> |
… |
|
string(const
string& rhs); |
// costr. per
copia |
string(string&& rhs); |
// costr. per
spostamento |
… |
|
}; |
Nell’elenco di inizializzazione dei membri del
costruttore di Annotation
, il
risultato di std::move(text)
è un
rvalue di tipo const std::string
.
Tale rvalue non può essere passato al costruttore per spostamento
di std::string
, poiché il costruttore
per spostamento prende un riferimento rvalue a una std::string
non-const
. L’r value può, tuttavia, essere passato
al costruttore per copia, poiché a un rvalue const
può associarsi a un riferimento lvalue a
const.
L’inizializzazione del membro,
pertanto, richiama il costruttore per copia
di std::string,
anche se text
è stato convertito in un rvalue! Tale
comportamento è fondamentale per mantenere la correttezza in
termini di const
. Lo spostamento di
un valore fuori da un oggetto, generalmente, è per modificare tale
oggetto, e pertanto il linguaggio non dovrebbe mai permettere che
gli oggetti const
vengano passati a
delle funzioni (come i costruttori per copia) che potrebbero
modificarli.
Da questo esempio si possono trarre due
lezioni. Innanzitutto, non si devono dichiarare oggetti
const
se poi si vuole avere la
possibilità di spostarli. Le richieste di spostamento su oggetti
const
vengono, di fatto, trasformate
in operazioni di copia. In secondo luogo, non solo std::move
in realtà non sposta nulla, ma non
garantisce neppure che l’oggetto che sta convertendo sia
spostabile. L’unica cosa davvero certa sul risultato
dell’applicazione di std::move
a un
oggetto è il fatto che si ottiene un rvalue.
A std::forward
si applica un ragionamento simile a quello per std::move,
ma mentre std::move
converte incondizionatamente il proprio argomento in un
rvalue, std ::forward
lo fa solo in
alcuni casi. std::forward
è una
conversione condizionale. Per capire quando
esegue e quando non esegue la conversione, bisogna considerare il
modo in cui std::forward
viene
normalmente utilizzata. La situazione più comune è un template di
funzione che accetta un parametro riferimento universale che deve
essere passato a un’altra funzione:
void process(const
Widget& lvalArg); |
// elabora gli
lvalue |
|
void
process(Widget&& rvalArg); |
// elabora gli
rvalue |
|
template<typename
T> |
// template che
passa |
|
void
logAndProcess(T&&
param) |
// i parametri da
elaborare |
|
{ |
||
auto now
= |
// ora
corrente |
|
std::chrono::system
clock::now(); |
||
makeLogEntry(“Calling ‘process’”, now); |
||
process(std::forward<T>(param)); |
||
} |
Considerate due chiamate a logAndProcess
: una con un lvalue e l’altra con un
rvalue:
Widget w; |
|
logAndProcess(w); |
// chiamata con
lvalue |
logAndProcess(std::move(w)); |
// chiamata con
rvalue |
All’interno di logAndProcess,
il parametro param
viene passato alla funzione process.
Quest’ultima ha un overloading per
lvalue e un altro per rvalue. Quando si richiama logAndProcess
con un lvalue, ci aspettiamo
naturalmente che tale lvalue venga inoltrato a process
come un lvalue e quando richiamiamo
logAndProcess
con un rvalue, ci
aspettiamo che venga richiamato l’overloading di process
per gli rvalue.
Ma
param,
come tutti i parametri di una
funzione, è un lvalue. Ogni chiamata a process
all’interno di logAndProcess
vorrà pertanto richiamare
l’overloading lvalue di process.
Per
evitare ciò, abbiamo bisogno di un meccanismo grazie al quale
param
possa essere convertito in un
rvalue se (e solo se) l’argomento con cui è stato inizializzato
param
(l’argomento passato a
logAndProcess)
era un rvalue. Questo
è esattamente ciò che fa std::forward.
Questo è il motivo per cui
std::forward
è una conversione
condizionale: converte in un rvalue solo se
il suo argomento è stato inizializzato con un rvalue.
Potreste chiedervi come faccia std::forward
a sapere se il suo argomento è stato
inizializzato con un rvalue. Nel codice precedente, per esempio,
come può std::forward
sapere se
param
è stato inizializzato con un
lvalue o con un rvalue? La risposta breve è che tale informazione
viene codificata nel parametro T
del
template di logAndProcess.
Il
parametro viene passato a std::forward,
che estrae l’informazione
codificata. Per i dettagli dell’esatto funzionamento di questo
meccanismo, consultate l’Elemento 28.
Dato che sia std::move
sia std::forward,
alla fine, non sono altro che
conversioni, dove l’unica differenza consiste nel fatto che
std::move
esegue la conversione
sempre, mentre std::forward
la esegue solo qualche volta, potreste chiedervi se non si possa
lasciar perdere std::move
e
utilizzare sempre std::forward,
ovunque. Dal punto di vista puramente tecnico, la risposta è
affermativa: std::forward
può fare
tutto e std ::move
non è necessaria.
Naturalmente nessuna delle due funzioni è davvero necessaria,
poiché potremmo anche scrivere direttamente le conversioni, ma
credo che siamo tutti d’accordo sul fatto che questo sarebbe...
indesiderabile.
I punti di forza di std::move
sono la comodità, il fatto che riduce
l’insorgere di errori e la maggiore chiarezza. Considerate una
classe in cui vogliamo determinare quante volte viene richiamato il
costruttore per spostamento. Predisponiamo un semplice contatore
static
che viene incrementato durante
la costruzione per spostamento. Supponendo che l’unico dato non
statico della classe sia una std::string,
ecco il modo convenzionale (ovvero
utilizzando std::move
) per
implementare il costruttore per spostamento:
class Widget
{
public:
Widget(Widget&&
rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls;
}
…
private:
static std::size_t
moveCtorCalls;
std::string
s;
};
Per implementare lo stesso comportamento con
std::forward,
il codice avrebbe il
seguente aspetto:
class Widget
{ |
|
public: |
|
Widget(Widget&& rhs) |
//
unconventional, |
: s(std::forward<std::string>(rhs.s)) |
// indesiderabile |
{ ++moveCtorCalls;
} |
//
implementation |
… |
|
}; |
Notate innanzitutto che std::move
richiede un solo argomento (rhs.s),
mentre std
::forward
richiede sia un argomento funzione (rhs.s)
sia un argomento di tipo template
(std::string).
Poi notate che il tipo
che passiamo a std::forward
non dovrà
essere un riferimento, poiché per convenzione la codifica
dell’argomento passato deve essere un rvalue (vedi Elemento 28).
Ciò significa che std::move
richiede
meno impegno di digitazione rispetto a std::forward
e ci evita anche la necessità di
passare un argomento di tipo che codifica il fatto che l’argomento
che stiamo passando è un rvalue. Inoltre, elimina la possibilità
del passaggio di un tipo errato (per esempio std ::string&,
che risulterebbe dal fatto che
il dato membro s
venga costruito per
copia anziché per spostamento).
Ancora più importante: l’uso di std::move
veicola la richiesta di una conversione
incondizionata in un rvalue, mentre l’uso di std::forward
veicola la richiesta di una
conversione in un rvalue solo per i riferimenti cui sono associati
degli rvalue. Si tratta di due azioni molto differenti. La prima,
tipicamente, prevede uno spostamento, mentre la seconda non fa
altro che passare (inoltrare) un oggetto a
un’altra funzione in un modo che mantiene le sue caratteristiche
originali: lvalue o rvalue. Poiché si tratta di azioni così
differenti, è giusto che esistano due funzioni differenti (con nomi
differenti).
Argomenti da ricordare
• std::move
esegue una conversione
incondizionata in una rvalue e di per se stessa non sposta
nulla.
• std::forward
converte
il proprio argomento in un rvalue solo se a l’argomento è associato
a un rvalue.
• Né std::move
né std::forward
svolgono alcuna operazione
runtime.
Elemento 24 – Distinguere i riferimenti universali e riferimenti rvalue
Si dice che la verità renda liberi, ma, in alcuni casi, una bugia ben scelta può essere altrettanto liberatoria. Questo Elemento è una bugia di questo tipo. Ma, in termini software, al posto di “bugia” si usa un termine più elegante: “astrazione”.
Per dichiarare un riferimento rvalue a un
determinato tipo T,
si scrive
T&&.
Sembra pertanto
ragionevole presumere che se si trova T&&
nel codice sorgente, si tratti di un
riferimento rvalue.
Purtroppo le cose non sono così semplici:
void
f(Widget&& param); |
// riferimento
rvalue |
WidgetSS var1 = Widget(); |
// riferimento
rvalue |
autoSS var2 = var1; |
// non è riferimento rvalue |
template<typename
T> |
|
void
f(std::vector<T>SS
param); |
// riferimento
rvalue |
template<typename
T> |
|
void f(TSS param); |
// non è riferimento rvalue |
In realtà, T&&
ha due significati differenti. Uno,
naturalmente, è un riferimento rvalue. Tali riferimenti si
comportano esattamente nel modo previsto: si associano solo a
rvalue e il loro motivo di esistere è
proprio quello di indentificare gli oggetti da cui possono essere
eseguiti spostamenti.
L’altro significato di T&&
è quello di riferimento rvalue oppure lvalue. Tali riferimenti, nel codice
sorgente, hanno entrambi l’aspetto di riferimenti rvalue (ovvero
T&&),
ma possono comportarsi
come se fossero riferimenti lvalue (ovvero T&).
Questa doppia natura permette loro di
associarsi a rvalue (come riferimenti rvalue) o anche a lvalue
(come riferimenti lvalue). Inoltre, possono associarsi a oggetti
const
o non-const,
a oggetti
volatile
o non-volatile
e perfino a
oggetti const
e volatile.
Praticamente si possono legare a
qualsiasi cosa. Tali riferimenti
assolutamente flessibili meritano un nome tutto loro. Possiamo
chiamarli riferimenti universali.11
I riferimenti universali si hanno in due contesti. Il più comune è quello dei parametri di template di funzioni, come questo esempio basato sul codice precedente:
template<typename
T> |
|
void f(TSS param); |
// param è un
riferimento universale |
Il secondo contesto è costituito dalle
dichiarazioni auto,
inclusa la
seguente, sempre tratta dal codice precedente:
auto&& var2 =
var1; // var2
è un riferimento universale
Questi due contesti hanno in comune la
presenza della deduzione del tipo. Nel
template di f,
il tipo di
param
viene dedotto e, nella
dichiarazione di var2,
il tipo di
var2
viene dedotto. Confrontate
questo con i seguenti esempi (sempre basati sul codice precedente),
dove manca la deduzione del tipo. Se vedete T&&
senza la deduzione del tipo, state
osservando un riferimento rvalue:
void f(WidqetSS param); |
// nessuna deduzione
del tipo; |
// param è un
riferimento rvalue |
|
WidgetSS vari = Widqet(); |
// nessuna deduzione
del tipo; |
// vari è un
riferimento rvalue |
Poiché i riferimenti universali sono “riferimenti”, devono essere inizializzati. È proprio l’inizializzatore di un riferimento universale a determinare il fatto che esso rappresenti un riferimento rvalue o lvalue. Se l’inizializzatore è un rvalue, il riferimento universale corrisponde a un riferimento rvalue. Se l’inizializzatore è un lvalue, il riferimento universale corrisponde a un riferimento lvalue. Per i riferimenti universali che sono parametri di funzione, l’inizializzatore viene fornito al momento della chiamata:
template<typename
T> |
|
void f(T&&
param); |
// param è un
riferimento universale |
Widget w; |
|
f(w); |
// lvalue passato a
f; il tipo di param |
// è Widqet&
(ovvero un riferimento lvalue) |
|
f(std::move(w) ); |
// rvalue passato a
f; il tipo di param |
// è Widqet&&
(ovvero un riferimento rvalue) |
Perché un riferimento possa essere universale,
la deduzione del tipo è necessaria, ma non sufficiente. Anche la
forma della dichiarazione del riferimento
deve essere corretta e tale forma deve sottostare a precisi
vincoli. Deve essere esattamente T&&.
Osservate nuovamente questo esempio
basato sul codice che abbiamo visto in precedenza:
template<typename
T> |
|
void f(std::vector<T>&& param); |
// param è un
riferimento rvalue |
Quando viene richiamata f
, il tipo T
viene
dedotto (a meno che il chiamante non lo specifichi esplicitamente,
un caso limite di cui non ci occuperemo). Ma la forma della
dichiarazione del tipo di param
non è
T&&,
bensì std::vector<T>&&.
Ciò elimina la
possibilità che param
sia un
riferimento universale. Pertanto param
sarà un riferimento rvalue, qualcosa che il
compilatore sarà felice di comunicarvi se tenterete di passare a
f
un lvalue:
std::vector<int> v; |
|
f(v); |
// errore! Non si può
associare un lvalue |
// a un riferimento
rvalue |
Anche la semplice presenza del qualificatore const è sufficiente per impedire che un riferimento possa essere universale:
template<typename
T> |
|
void f(const T&& param); |
// param è un
riferimento rvalue |
Se vi
trovate in un template e vedete un parametro di funzione di tipo
T&&,
potete essere portati a
supporre che si tratti di un riferimento universale. Ma non è così.
Il fatto che sia un template non garantisce la deduzione del tipo.
Considerate la seguente funzione membro push_back
in std::vector:
template<class T,
class Allocator = allocator<T>> |
// dal
C++ |
class vector
{ |
//
Standard |
public: |
|
void push
back(T&& x); |
|
… | |
}; |
Il parametro di push_back
ha certamente la forma corretta per un
riferimento universale, ma in questo caso non vi è la deduzione del
tipo. Questo perché push_back
non può
esistere senza una particolare istanziazione di vector
di cui possa far parte e il tipo di tale
istanziazione determina completamente la dichiarazione per
push_back.
Pertanto, dicendo
std::vector<Widget> v;
si fa in modo che il template std::vector
venga istanziato nel seguente
modo:
class
vector<Widget, allocator<Widget>> { |
|
public: |
|
void push
back(Widget&& x); |
// riferimento
rvalue |
… |
|
}; |
Ora si può vedere chiaramente che pushback
non impiega la deduzione del tipo.
Questa push_back
per vector<T>
(ve sono due, la funzione subisce
overloading) dichiara sempre un parametro di tipo riferimento
rvalue a T.
Al contrario, la funzione membro emplace_back,
concettualmente simile, in
std ::vector
impiega la deduzione del tipo:
template<class T,
class Allocator = allocator<T>> |
// sempre
dal |
class vector
{ |
// C++ |
public: |
//
Standard |
template
<class... Args> |
|
void emplace
back(ArgsSS... args); |
|
… |
|
}; |
Qui, il parametro di tipo Args
è indipendente dal parametro del tipo,
T,
di vector
e pertanto Args
deve essere dedotto ogni volta che viene
richiamata emplace_back
(d’accordo,
in realtà Args
non è esattamente un
parametro per il tipo, ma per gli scopi di questa discussione,
possiamo trattarlo come tale).
Il fatto che il parametro per il tipo di
emplace_back
si chiami Args
e che comunque sia un riferimento universale
va a supporto del mio precedente commento: che è la forma di un
riferimento universale che deve essere T&&.
Non è obbligatorio utilizzare il
nome T.
Per esempio, il seguente
template prende un riferimento universale, poiché la forma
(type&&)
è corretta e il tipo
di param
verrà dedotto (nuovamente,
escludendo il caso limite in cui il chiamante specifichi
esplicitamente il tipo):
template<typename
MyTemplateType> |
// param è
un |
void
someFunc(MyTemplateType&&
param); |
// riferimento
universale |
Ho detto in precedenza che anche le variabili
auto
possono essere riferimenti
universali. Per essere più precisi, le variabili dichiarate di tipo
auto&&
sono riferimenti
universali, poiché a esse si applica la deduzione del tipo e hanno
la forma corretta (T&&).
I
riferimenti universali auto
non sono
altrettanto comuni dei riferimenti universali utilizzati per i
parametri dei template di funzione, ma ogni tanto vengono impiegati
in C++11. Sono invece più presenti in C++14, perché le espressioni
lambda del C++14 possono dichiarare parametri auto&&.
Per esempio, se voleste scrivere
una lambda C++14 per registrare il tempo impiegato nella chiamata
di una funzione arbitraria, potreste fare nel seguente modo:
auto
timeFuncInvocation = |
|
[](auto&& func, auto&&... params) |
// C++14 |
{ |
|
start timer; |
|
std::forward<decltype(func)>(func)( |
// richiama
func |
std::forward<decltype(params)>(params)... |
// su
params |
); |
|
stop timer and record elapsed time; |
|
}; |
Se la vostra reazione al codice std::forward<decltype(
bla
bla bla)> all’interno della lambda è “Ma che diamine...?!”,
questo significa che probabilmente non avete ancora letto
l’Elemento 33.
Non preoccupatevi. L’importante in questo Elemento è costituito dai
parametri auto&&
dichiarati
dalla lambda. func
è un riferimento
universale che può essere associato a ogni oggetto richiamabile,
che sia lvalue o rvalue. args
è
costituito da zero o più riferimenti universali (ovvero un
pack) che può essere associato a un
qualsiasi numero di oggetti di tipo arbitrario. Il risultato,
grazie ai riferimenti universali auto,
è che timeFuncInvocation
può controllare l’esecuzione
di praticamente ogni funzione (per dettagli
su questa differenza fra “ogni” e “praticamente ogni”, consultate
l’Elemento
30).
Tenete in considerazione che questo intero
Elemento (alla base dei riferimenti universali) è una bugia... ops,
volevo dire una “astrazione”. La verità su cui essa si basa è
chiamata collasso dei riferimenti (reference
collapsing), un argomento al quale è dedicato l’Elemento 28.
Ma la verità non rende meno utile l’astrazione. Il fatto di
distinguere tra riferimenti rvalue e riferimenti universali vi
aiuterà a leggere il codice sorgente con maggiore precisione (“Il
T&&
che ho davanti si associa
solo a rvalue o a qualsiasi cosa?”) e vi eviterà di comunicare in
modo ambiguo con i colleghi (“Sto utilizzando un riferimento
universale, non un riferimento rvalue...”). Aiuterà inoltre a
comprendere gli Elementi 25 e
26, che contano su questa distinzione. Accettate dunque questa
astrazione. Come le leggi sulla gravitazione di Newton
(tecnicamente errate) sono altrettanto utili e più facili da
applicare rispetto alla teoria della relatività generale di
Einstein (la “verità”), così il concetto di riferimento universale
è normalmente preferibile a considerare i dettagli del
“collasso”.
Argomenti da ricordare
• Se un parametro di un template
di funzione ha tipo T&&
per
un tipo dedotto T
o se un oggetto è
dichiarato utilizzando auto&&
, il parametro o l’oggetto è un
riferimento universale.
• Se la forma della dichiarazione del tipo non è esattamente type&& o se la deduzione del tipo non si verifica, type&& identifica un riferimento rvalue.
• I riferimenti universali corrispondono a riferimenti rvalue se vengono inizializzati tramite un rvalue. Corrispondono invece a riferimenti lvalue se vengono inizializzati con un lvalue.
Elemento 25 – Usare std::move sui riferimenti rvalue e std::forward sui riferimenti universali
I riferimenti rvalue si associano solo a oggetti che sono candidati per lo spostamento. Se avete un parametro riferimento rvalue, sapete che l’oggetto cui si associa può essere spostato:
class Widget
{ |
|
Widget(WidgetSS rhs); |
// rhs fa assolutamente riferimento |
… |
// a un oggetto
spostabile |
}; |
Per questo motivo, vorrete passare tali oggetti ad altre funzioni in un modo che permetta a tali funzioni di sfruttare il fatto che l’oggetto è un rvalue. Il modo per farlo consiste nel convertire in rvalue i parametri associati a tali oggetti. Come descrive l’Elemento 23, questo non è solo ciò che std::move fa, è anche il motivo stesso per cui è stata creata:
class Widget
{ |
|
public: |
|
Widget(Widget&& rhs) |
// rhs è un
riferimento rvalue |
: name(std::move(rhs.name)), |
|
p(std::move(rhs.p)) |
|
{ ... } |
|
… |
|
private: |
|
std::string
name; |
|
std::shared_ptr<SomeDataStructure> p; |
|
}; |
Un
riferimento universale, al contrario (vedi Elemento 24),
può essere associato a un oggetto che può
essere impiegato per uno spostamento. I riferimenti universali
devono essere convertiti in rvalue solo se sono stati inizializzati
tramite un rvalue. L’Elemento 23 spiega che questo è esattamente ciò che
fa std::forward
:
class Widget
{ |
|
public: |
|
template<typename
T> |
|
void
setName(T&& newName) |
// newName è
un |
{ name = std::forward<T>(newName); } |
// riferimento
universale |
… | |
}; |
In breve, i riferimenti rvalue dovrebbero
essere convertiti incondizionatamente in
rvalue (tramite std::move)
quando
vengono inoltrati ad altre funzioni, poiché sono sempre associati a rvalue; i riferimenti universali
dovrebbero essere convertiti
condizionatamente in rvalue (tramite std::forward)
al momento dell’inoltro, poiché
solo talvolta sono associati a rvalue.
L’Elemento 23 spiega che l’uso di std::forward
su riferimenti rvalue può anche
esibire un comportamento corretto, ma il codice sorgente diviene
prolisso, soggetto a errori e di difficile comprensione; pertanto
si dovrebbe evitare di utilizzare std::forward
con riferimenti rvalue. Ancora
peggiore è l’idea di utilizzare std::move
con i riferimenti universali, poiché
possono avere l’effetto di modificare inaspettatamente gli lvalue
(ovvero le variabili locali):
class Widget
{ |
|
public: |
|
template<typename
T> |
|
void
setName(T&& newName) |
// riferimento
universale |
{ name = std::move(newName); } |
// compilabile,
ma |
… |
// decisamente cattiva programmazione! |
private: |
|
std::string
name; |
|
std::shared_ptr<SomeDataStructure> p; |
|
}; |
|
std::string
getWidgetName(); |
// funzione
factory |
Widget w; |
|
auto n =
getWidgetName(); |
// n è una variabile
locale |
w.setName(n); |
// sposta n in w! |
… |
// il valore di n ora
è ignoto |
Qui, la variabile locale n
viene passata a w.setName;
il chiamante può erroneamente supporre
che si tratti di un’operazione di sola lettura su n.
Ma poiché setName
usa internamente std::move
per convertire incondizionatamente il
suo parametro riferimento in un rvalue, il valore di n
verrà spostato in w.name
e dopo la chiamata a set-Name, n
avrà un valore non specificato. Questo è un tipo di comportamento
che genera collera nel chiamante.
Si potrebbe supporre che il parametro di
setName
non dovrebbe essere
dichiarato come riferimento universale. Tali riferimenti non
possono essere const
(vedi Elemento 24),
e certamente setName
non dovrebbe
certo modificare il proprio parametro. Potreste pensare che se
setName
avesse semplicemente subito
overloading per lvalue const
e per
rvalue, tutto questo problema avrebbe potuto essere evitato. Nel
seguente modo:
class Widget
{ |
|
public: |
|
void
setName(const std::string&
newName) |
// impostato
da |
{ name = newName;
} |
// const
lvalue |
void
setName(std::string&&
newName) |
// impostato
da |
{ name =
std::move(newName); } |
// rvalue |
… |
|
}; |
Questo certamente potrebbe funzionare (in
questo caso), ma presenta dei problemi. Innanzitutto, vi è
ulteriore codice sorgente da scrivere e correggere (due funzioni
invece di un unico template). In secondo luogo, può essere meno
efficiente. Per esempio, considerato il seguente uso di
setName:
w.setName(“Adela
Novak”);
Con la versione di setName
che accetta un riferimento universale, a
setName
verrebbe passata la stringa
letterale “Adela Novak”,
la quale
verrebbe inviata all’operatore di assegnamento per la std::string
all’interno di w.
Il dato membro w
di name
verrebbe pertanto assegnato
direttamente dal letterale stringa; non ne deriverebbero oggetti
std::string
temporanei. Con le
versioni in overloading di setName,
invece, verrebbe creato un oggetto setName
temporaneo cui associare il parametro di
setName,
e questa std::string
temporanea verrebbe poi spostata nel
dato membro di w.
Una chiamata a
setName
richiederebbe, pertanto,
l’esecuzione di: un costruttore di std::string
(per creare la stringa temporanea),
un operatore di assegnamento per spostamento di std::string
(per spostare newName
in w.name)
e un distruttore di std::string
(per
distruggere la stringa temporanea). Questa è quasi certamente una
sequenza di esecuzione più costosa rispetto alla semplice chiamata
dell’operatore di assegnamento di std::string,
che accetta un puntatore
const char*.
Il costo aggiuntivo
varia da implementazione a implementazione e quanto occorra
preoccuparsi di questo costo varia da applicazione ad applicazione
e da libreria a libreria, ma il fatto è che la sostituzione di un
template che accetta un riferimento universale con una coppia di
funzioni in overloading per riferimenti lvalue e riferimenti
rvalue, molto probabilmente prevedrà dei costi runtime. Se poi
generalizziamo l’esempio, in modo che il dato membro di
Widge
t possa essere di un tipo
arbitrario (ovvero non abbiamo la garanzia che si tratti di una
std::string),
il divario
prestazionale può ampliarsi notevolmente, poiché non tutti i tipi
sono “economici” da spostare quanto le std::string
(vedi Elemento
29).
Il
problema più serio con l’overloading per lvalue e rvalue, tuttavia,
non riguarda la quantità o la comprensibilità del codice sorgente,
e neppure le prestazioni runtime del codice. È la cattiva
scalabilità del progetto. Widget::setName
accetta un solo parametro e
pertanto richiede due soli overload, ma per le funzioni che
richiedono più parametri, ognuno dei quali potrebbe essere un
lvalue o un rvalue, il numero di overloading cresce
geometricamente: con n parametri saranno
necessari 2n overload. Ma c’è di
peggio. Alcune funzioni (in realtà template di funzioni) accettano
un numero illimitato di parametri, ognuno
dei quali potrebbe essere un lvalue o un rvalue. Il classico
esempio è rappresentato da std::make_shared
e, in C++14, da std::make_unique
(vedi Elemento 21). Osservate
le dichiarazioni degli overload utilizzati più comunemente:
template<class T,
class... Args> |
// dal
C++11 |
shared ptr<T>
make shared(ArgsSS... args); |
//
Standard |
template<class T,
class... Args> |
// dal
C++14 |
unique_ptr<T>
make unique(Args&&...
args); |
//
Standard |
Per funzioni come queste, l’overloading per
lvalue e rvalue non è possibile: i riferimenti universali sono
l’unico modo di procedere. E all’interno di tali funzioni, ve lo
garantisco, ai parametri riferimento universale passati ad altre
funzioni viene applicata std::for-ward.
Questo è esattamente ciò che
dovreste fare.
Almeno di solito. Talvolta. Ma non
necessariamente per principio. In alcuni casi, intendete utilizzare
l’oggetto associato a un riferimento rvalue o a un riferimento
universale più di una volta in un’unica funzione e volete
assicurarvi che non venga spostato finché non avrete terminato di
utilizzarlo. In tal caso, dovrete applicare std::move
(per i riferimenti rvalue) o
std::forward
(per i riferimenti
universali) solo all’ultimo utilizzo del riferimento. Per
esempio:
template<typename
T> |
// text è |
void
setSignText(T&& text) |
// rif.
univ. |
{ |
|
sign.setText(text); |
// usa text,
ma |
// senza
modificarlo |
|
auto now
= |
// ottiene l’ora
corrente |
std::chrono::system
clock::now(); |
|
signHistory.add(now, |
|
std::forward<T>(text)); |
// conversione
condizionale |
} |
// di text in
rvalue |
Qui vogliamo assicurarci che il valore di
text
non venga cambiato da
sign.setText,
poiché vogliamo
utilizzare tale valore quando richiamiamo signHistory.add.
Da qui l’uso di std::forward
solo sull’utilizzo finale del
riferimento universale.
Per std::move
si
applica lo stesso ragionamento (ovvero applicare std::move
a un riferimento rvalue solo l’ultima
volta che viene utilizzato), ma è importante notare che in alcuni
rari casi si vuole richiamare std::move_if_noexcept
invece di std::move.
Per capire quando e perché, consultate
l’Elemento
14.
Se siete
in una funzione che restituisce per valore
e state restituendo un oggetto associato a un riferimento rvalue o
a un riferimento universale, dovrete applicare std::move
o std::forward
al momento del return
del riferimento. Per capire perché,
considerate una funzione operator+
con la quale sommare due matrici, dove la matrice di sinistra si sa
che è un rvalue (il suo spazio può pertanto essere riutilizzato per
contenere la somma delle matrici):
Matrix |
// return
per-valore |
operator+(MatrixSS lhs, const MatrixS rhs) |
|
{ |
|
lhs +=
rhs; |
|
return std::move(lhs); |
// sposta lhs nel |
} |
// valore
restituito |
Convertendo lhs in un rvalue nell’istruzione return (tramite std::move), lhs verrà spostato nella posizione del valore restituito dalla funzione. Se la chiamata std::move venisse omessa,
Matrix |
// come
sopra |
operator+(Matrix&& lhs, const Matrix&
rhs) |
|
{ |
|
lhs +=
rhs; |
|
return lhs; |
// copia lhs nel |
} |
// valore
restituito |
il fatto che lhs
è un lvalue forzerebbe i compilatori a copiarlo, invece, nella posizione del valore
restituito. Supponendo che il tipo Matrix
supporti la costruzione per spostamento,
più efficiente rispetto alla costruzione per copia, l’uso di
std::move
nell’istruzione
return
genera codice più
efficiente.
Se Matrix
non
supporta lo spostamento, la sua conversione in un rvalue non darà
problemi, poiché tale rvalue verrà semplicemente copiato dal
costruttore per copia di Matrix
(vedi
Elemento
23). Se però Matrix
viene
successivamente modificato per supportare lo spostamento,
operator+
se ne avvantaggerà
automaticamente la prossima volta che verrà compilato. Per questo
motivo, non si perderà nulla (anzi, si avrà un vantaggio)
applicando std::move
ai riferimenti
rvalue restituiti dalle funzioni che restituiscono per valore.
La situazione è simile per i riferimenti
universali e std::forward.
Considerate un template di funzione reduceAndCopy
che accetta un oggetto Fraction
potenzialmente non ridotto, lo riduce e
poi restituisce una copia del valore ridotto. Se l’oggetto
originale è un rvalue, il suo valore dovrà essere spostato nel
valore restituito (evitando così il costo dell’operazione di
copia); ma se l’originale è un lvalue, deve essere eseguita una
copia. Pertanto:
Se la chiamata a std::forward
venisse omessa, frac
verrebbe incondizionatamente copiato nel
valore restituito da reduceAndCopy.
Alcuni programmatori usano questa informazione
e tentano di estenderla anche alle situazioni in cui non si
applica: “Se l’uso di std::move
su un
parametro riferimento rvalue da copiare nel valore restituito
trasforma una costruzione per copia in una costruzione per
spostamento”, pensano, “posso eseguire la stessa operazione anche
sulle variabili locali che restituisco”. In altre parole,
immaginano che, data una funzione che restituisce una variabile
locale per valore, come il seguente esempio,
Widget
makeWidget() |
// Cersione per
“copia” di makeWidget |
{ |
|
Widget w; |
// variabile
locale |
… | // configura
w |
return w; |
// “copia” w nel
valore restituito |
} |
|
intenderebbero “ottimizzarla” trasformando la “copia” in uno spostamento:
Widget
makeWidget() |
// versione per
spostamento di makeWidget |
{ |
|
Widget w; |
|
… | |
return std::move(w); |
// sposta w nel
valore restituito |
} |
// (ASSOLUTAMENTE DA
NON FARE!) |
Tutte le virgolette e i commenti che ho inserito dovrebbero chiarire che questo ragionamento è errato. D’accordo, ma perché?
È errato poiché il Comitato di
Standardizzazione è sempre un passo avanti ai programmatori quando
si tratta di eseguire ottimizzazioni. È stato riconosciuto molto
tempo fa che la versione per “copia” di makeWidget
può evitare la necessità di copiare la
variabile locale w
costruendola nella
memoria allocata per il valore che verrà restituito dalla funzione.
Questa tecnica è chiamata Return Value
Optimization (RVO) ed è espressamente suggerita dallo standard
del C++ fin da quando esiste.
In realtà si vuole permettere tale elisione della copia solo nei punti in cui ciò non influenzi il comportamento osservabile del software. Per rendere più comprensibile il linguaggio impiegato dallo Standard, i compilatori possono elidere la copia (o lo spostamento) di un oggetto locale12 in una funzione che restituisce per valore se (1) il tipo dell’oggetto locale coincide con il tipo restituito dalla funzione e (2) l’oggetto locale e ciò che viene restituito.
Detto questo, osservate nuovamente la versione
per “copia” di makeWidget
:
Widget
makeWidget() |
// versione per
“copia” di makeWidget |
{ |
|
Widget w; |
|
… | |
return w; |
// “copia” w nel
valore restituito |
} |
Qui entrambe le condizioni sono soddisfatte e
potete fidarvi se vi dico che per questo codice, ogni compilatore
C++ degno di questo nome impiegherà l’ottimizzazione RVO per
evitare di copiare w.
Questo
significa che la versione per “copia” di makeWidget,
in realtà, non copia nulla.
La versione per spostamento di makeWidget
fa esattamente ciò che dice il nome
(supponendo che Widget offra un costruttore per spostamento):
sposta il contenuto di w
nella
posizione del valore restituito da makeWidget.
Ma perché i compilatori non usano
l’ottimizzazione RVO per eliminare lo spostamento, costruendo
nuovamente w
nella memoria allocata
per il valore restituito dalla funzione? La risposta è semplice:
non possono. La condizione (2) stabilisce che l’ottimizzazione RVO
possa essere eseguita solo se ciò che viene restituito è un oggetto
locale, ma questo non è ciò che la versione per spostamento di
makeWidget
sta facendo. Osservate
nuovamente la sua istruzione return
:
return std::move(w);
Ciò che viene restituito qui non è l’oggetto
locale w,
ma un
riferimento a w: il risultato di std::move(w).
La restituzione di un riferimento a
un oggetto locale non soddisfa le condizioni necessarie per
l’ottimizzazione RVO, pertanto i compilatori devono spostare
w
nella posizione del valore
restituito dalla funzione. Gli sviluppatori che cercano di aiutare
i loro compilatori a eseguire un’ottimizzazione applicando
std::move
a una variabile locale che
viene restituita, stanno in realtà limitando le opzioni di
ottimizzazione già disponibili per i loro compilatori!
Tuttavia la RVO è un’ottimizzazione. I
compilatori non sono obbligati a elidere le
operazioni di copia e spostamento, anche quando lo possono fare. I
più paranoici possono temere che i compilatori vi puniranno per le
operazioni di copia, per il semplice fatto che possono. O forse
avete conoscenze sufficienti per capire che vi sono casi in cui
l’ottimizzazione RVO è troppo complessa da implementare per i
compilatori, per esempio quando diversi percorsi di controllo di una funzione
restituiscono variabili locali differenti (i compilatori dovrebbero
generare codice per costruire la variabile locale appropriata nella
memoria destinata al valore restituito dalla funzione, ma come
potrebbero i compilatori determinare quale variabile locale sarebbe
appropriata?). In tal caso, potreste voler pagare il prezzo di uno
spostamento, considerandolo un’assicurazione rispetto al costo di
una copia. Pertanto potreste comunque considerare che è ragionevole
applicare std::move
a un oggetto
locale che state restituendo, semplicemente perché sapete che non
pagherete mai per una copia.
In tal caso, l’applicazione di std::move
a un oggetto locale sarebbe comunque una cattiva idea. La parte dello Standard
che suggerisce l’ottimizzazione RVO dice anche che se le condizioni
di tale ottimizzazione sono verificate, ma i compilatori scelgono
di non eseguire l’elisione dell’operazione di copia, l’oggetto
restituito deve essere trattato come un
rvalue. In pratica, lo Standard richiede che quando
l’ottimizzazione RVO è permessa, o si svolge l’elisione della
copia, oppure agli oggetti locali restituiti viene implicitamente
applicata std::move.
Pertanto, nella
versione per “copia” di makeWidget
,
Widget
makeWidget() |
// come
prima |
{ |
|
Widget
w; |
|
… | |
return
w; |
|
} |
i compilatori devono elidere la copia di
w
o devono trattare la funzione come
se fosse scritta nel seguente modo:
Widget
makeWidget() |
|
{ |
|
Widget
w; |
|
… | |
return std::move(w); |
// tratta w come un
rvalue, poiché |
} |
// non è stata
eseguita l’elisione della copia |
La situazione è simile per i parametri di funzione passati per valore. In questo caso non è prevista l’elisione della copia per il valore restituito dalla funzione, ma, se vengono restituiti, i compilatori devono trattarli come rvalue. Come risultato, se il codice sorgente ha il seguente aspetto:
Widget makeWidget(Widget
w) |
// parametro per
valore dello stesso |
{ |
// tipo del tipo
restituito dalla funzione |
… |
|
return w; |
|
} |
|
i compilatori devono trattarlo come se fosse scritto nel seguente modo:
Widget
makeWidget(Widget w) |
|
{ |
|
... |
|
return std::move(w); |
// tratta w come un
rvalue |
} |
Ciò significa che se si usa std::move
sull’oggetto locale restituito da una
funzione che sta restituendo per valore, non potete aiutare i
vostri compilatori (devono trattare l’oggetto locale come un rvalue
se non eseguono l’elisione della copia), ma certamente potete
ostacolarli (impedendo l’ottimizzazione RVO). Vi sono situazioni in
cui l’applicazione di std::move
a una
variabile locale può essere una scelta ragionevole (per esempio,
quando la passate a una funzione e sapete che non utilizzerete più
tale variabile), ma non per un’istruzione return
che si qualificherebbe per
un’ottimizzazione RVO o che restituisce un parametro per
valore.
Argomenti da ricordare
• Applicate std::move
ai riferimenti rvalue e std::forward
ai riferimenti universali solo
l’ultima volta che vengono utilizzati.
• Fate la stessa cosa per i riferimenti rvalue e per i riferimenti universali restituiti dalle funzioni che restituiscono per valore.
• Non applicate mai std::move
o std::forward
agli oggetti locali ottimizzabili
con RVO.
Elemento 26 – Evitare l’overloading sui riferimenti universali
Supponete di dover scrivere una funzione che accetta come parametro un nome, registra la data e l’ora, poi aggiunge il nome a una struttura dati globale. Il risultato potrebbe essere una funzione con il seguente aspetto:
std::multiset<std::string> names; |
// struttura dati
globale |
void logAndAdd(const
std::string& name) |
|
{ |
|
auto now
= |
// ottiene l’ora
corrente |
std::chrono::system
clock::now(); |
|
log(now,
“logAndAdd”); |
// crea la voce
log |
names.emplace(name); |
// aggiunge il nome
alla struttura |
} |
// dati globale; vedi
Elemento
42 |
// per info su
emplace |
Non è codice da “buttare via”, ma non è efficiente quanto dovrebbe. Considerate tre potenziali chiamate:
std::string
petName(“Darla”); |
|
logAndAdd(petName); |
// passa std::string
lvalue |
logAndAdd(std::string(“Persephone”)); |
// passa std::string
rvalue |
logAndAdd(“Patty Dog”); |
// passa una stringa
letterale |
Nella prima chiamata, il parametro
name
di logAndAdd
viene associato alla variabile
petName.
In logAndAdd, name
viene poi passato a names.emplace.
Poiché name
è un lvalue, viene copiato in names.
Non vi è alcun modo per evitare la copia,
poiché a logAndAdd
è stato passato un
lvalue (petName).
Nella seconda chiamata, il parametro
name
viene associato a un rvalue (la
std::string
temporanea creata
esplicitamente da “Persephone”). name
stesso è un lvalue, pertanto viene copiato in names,
ma riconosciamo che, in linea di
principio, il suo valore potrebbe essere spostato in names.
In questa chiamata, paghiamo per una
copia, ma potremmo riuscire a ottenere il tutto utilizzando solo
uno spostamento.
Nella terza chiamata, il parametro
name
viene nuovamente associato a un
rvalue, ma questa volta si tratta di una std::string
temporanea, che viene implicitamente
creata da “Patty Dog”
. Come nella
seconda chiamata, tutti i name
vengono copiati in names,
ma in
questo caso l’argomento originariamente passato a emplace
è una stringa letterale. Se la stringa
letterale fosse stata passata direttamente a emplace,
non vi sarebbe stata alcuna necessità di
creare una std::string
temporanea. Al
contrario, emplace
avrebbe usato la
stringa letterale per creare l’oggetto std::string
direttamente all’interno di
std::multiset.
In questa terza
chiamata, quindi, paghiamo per copiare una std::string
, ma in realtà non vi è alcun motivo
di pagare neppure per uno spostamento, tantomeno per una copia.
Possiamo eliminare le inefficienze nella
seconda e nella terza chiamata riscrivendo logAndAdd
in modo che accetti un riferimento
universale (vedi Elemento 24) e, sulla base dell’Elemento 25,
applicare std::forward
per inviare
questo riferimento a emplace.
Il
risultato parla da solo:
template<typename T> |
|
void
logAndAdd(T&& name) |
|
{ |
|
auto now
= std::chrono::system clock::now(); |
|
log(now,
“logAndAdd”); |
|
names.emplace(
std::forward<T>( name) ); |
|
} |
|
std::string
petName(“Darla”); |
// come
prima |
logAndAdd( petName); |
// come prima,
copia |
// lvalue in
multiset |
|
logAndAdd( std::string(“Persephone”) ); |
// sposta rvalue invece |
// di
copiarlo |
|
logAndAdd(“Patty Dog”); |
// crea std::string |
// in multiset invece |
|
// di copiare una
std::string |
|
//
temporanea |
|
Efficienza perfetta!
Se il discorso si fermasse qui, potremmo
fermarci e cambiare argomento, ma non abbiamo ancora parlato del
fatto che i client non sempre hanno un accesso diretto ai nomi
richiesti da logAndAdd.
Alcuni client
hanno solo un indice, utilizzato da logAndAdd
per ricercare il nome corrispondente
all’interno di una tabella. Per supportare tali client,
logAndAdd
subisce overloading:
std::string
nameFromIdx(int idx); |
// restituisce il
nome |
// corrispondente a
idx |
|
void
logAndAdd(int idx) |
// nuovo
overload |
{ |
|
auto now =
std::chrono::system_clock::now(); |
|
log(now,
“logAndAdd”); |
|
names.emplace(nameFromIdx(idx)); |
|
} |
La risoluzione delle chiamate ai due overloading funziona come previsto:
std::string
petName(“Darla”); |
// come
prima |
logAndAdd(petName); |
// come prima,
tutte |
logAndAdd(std::string(“Persephone”)); |
// queste chiamate
richiamano |
logAndAdd(“Patty
Dog”); |
// l’overload per
T&& |
logAndAdd(22); |
// richiama
l’overload per int |
In effetti, la risoluzione funziona come
previsto solo se non ci si aspetta troppo. Supponete che un client
abbia uno short
che contiene un
indice e che lo passi a logAndAdd
:
short
nameIdx; |
|
… |
// dare un valore a
nameIdx |
logAndAdd(nameIdx); |
//
errore! |
Il commento sull’ultima riga non è particolarmente illuminante, ecco dunque che cosa succede.
Vi sono due overload per logAndAdd.
Quello che accetta un riferimento
universale può dedurre T
come
short,
trovando pertanto una
corrispondenza esatta. L’overloading per un parametro int
può corrispondere all’argomento short
solo dopo una promozione. In base alle
classiche regole di risoluzione dell’overloading, una
corrispondenza esatta batte una corrispondenza per promozione,
pertanto viene richiamato l’overloading per il riferimento
universale.
All’interno di tale overloading, il parametro
name
viene associato allo
short
che gli viene passato.
name
viene poi inoltrato con
std::forward
alla funzione membro
emplace
su names
(una std::multiset<std::string>),
che, a sua
volta, lo inoltra diligentemente al costruttore di std::string.
Ma non esiste alcun costruttore per
std ::string
che accetti uno
short,
pertanto la chiamata al
costruttore di std::string
all’interno della chiamata multiset::emplace
all’interno della chiamata a
logAndAdd
non ha successo. Tutto
perché l’overloading per il riferimento universale era una
corrispondenza migliore per un argomento short
rispetto a un int.
Le funzioni che accettano riferimenti universali sono fra le più “fameliche” del C++. Si istanziano per creare corrispondenze esatte per quasi ogni tipo di argomento (i pochi argomenti che le “sfuggono” sono descritti nell’Elemento 30). Questo è il motivo per cui combinare l’overloading e i riferimenti universali è quasi sempre una cattiva idea: l’overloading per riferimenti universali si aggiudica molti più tipi di argomenti rispetto a quanti lo sviluppatore generalmente si immagini.
Un modo semplice per schivare questa trappola
consiste nello scrivere un costruttore perfect-forward. Una piccola
modifica all’esempio logAndAdd
illustra il problema. Invece di scrivere una funzione che può
prendere una std::string
o un indice
che possa essere utilizzato per ricercare una std::string,
immaginate una classe Person
con dei costruttori che facciano la stessa
cosa:
class Person
{ |
|
public: |
|
template<typename
T> |
|
explicit
Person(T&& n) |
// costr. per
perfect-forward; |
:
name(std::forward<T>(n)) {} |
// inizializza il
dato membro |
explicit Person(int
idx) |
// costr. per
int |
:
name(nameFromIdx(idx)) {} |
|
… |
|
private: |
|
std::string
name; |
|
}; |
Come nel caso di logAndAdd,
il passaggio di un tipo intero diverso
da int
(ovvero std::size_t, short, long
e così via) richiamerà
l’overloading del costruttore per riferimenti universali invece che
per int
e ciò porterà a un errore di
compilazione. Tuttavia, qui il problema è molto peggiore, poiché in
Person
vi sono molti più overloading
rispetto a quanto si possa immaginare. L’Elemento 17
spiega che in condizioni appropriate, il C++ genererà i costruttori
per copia e per spostamento e questo vale anche se la classe
contiene un costruttore template-izzato che
possa essere istanziato per produrre la signature del costruttore
per copia o per spostamento. Se i costruttori per copia e per
spostamento di Person
vengono
generati in questo modo, Person
avrà
il seguente aspetto:
Questo porta a un comportamento che può essere intuitivo solo per chi ha lavorato sui compilatori, tanto da aver dimenticato la sua essenza umana.
Person
p(“Nancy”); |
|
auto
cloneOfP(p); |
// crea una nuova
Person da p; |
// incompilabile! |
Qui stiamo tentando di creare una Person
da un’altra Person,
che sembra un caso davvero ovvio di
costruzione per copie (p
è un lvalue
e così possiamo fugare tutti i dubbi che possiamo avere sul fatto
che venga eseguita una “copia” attraverso un’operazione di
spostamento). Ma questo codice non richiamerà il costruttore per
copie. Richiamerà il costruttore perfect-forward. Tale funzione
tenterà poi di inizializzare il dato membro std::string
di Person
con un oggetto di tipo Person (p).
Poiché std::string
non ha alcun costruttore che accetta
un Person,
il compilatore alzerà le
mani esasperato, punendovi come sa fare: con lunghi e
incomprensibili messaggi di errore per esprimere tutto il proprio
disappunto.
“Perché”, potreste chiedervi, “viene
richiamato il costruttore per il perfect-forward invece del
costruttore per copia? Stiamo inizializzando una Person
con un’altra Person!”.
In effetti è così, ma i compilatori
sono soliti seguire le regole del C++ e le regole impiegate qui si
occupano proprio di governare la risoluzione delle chiamate alle
funzioni in overloading.
Il compilatore ragiona nel seguente modo.
cloneOfP
viene inizializzato con un
lvalue non-const
(p)
e ciò significa che il costruttore template-izzato può essere distanziato per prendere
un lvalue non-const
di tipo Person.
Dopo tale istanziazione, la classe
Person
ha il seguente aspetto:
class Person
{ |
|
public: |
|
explicit Person(Person& n) |
// istanziata
dal |
:
name(std::forward<Person&>(n)) {} |
//
template |
// per
perfect-forward |
|
explicit Person(int
idx); |
// come
prima |
Person(const Persons
rhs); |
// costr. per
copia |
// (generato dal
compilatore) |
|
}; |
|
auto
cloneOfP(p);
p potrebbe essere passato al costruttore per
copia o al template istanziato. La chiamata del costruttore per
copia richiederebbe l’aggiunta di const
a p
per
coincidere con il tipo di parametri del costruttore per copia,
mentre la chiamata del template istanziato non richiede questa
aggiunta. L’overloading generato dal template è pertanto una
corrispondenza migliore e così i compilatori faranno ciò che sono
progettati per fare: generare una chiamata alla funzione che offre
la migliore corrispondenza. La “copia” di lvalue non-const
di tipo
Person
viene pertanto gestita dal
costruttore per perfect-forward, non dal costruttore per copia.
Se cambiamo leggermente l’esempio, in modo che
l’oggetto da copiare sia const,
la
situazione è completamente differente:
const Person cp(“Nancy”); |
// ora l’oggetto è
const |
auto
cloneOfP(cp); |
// richiama il costr.
per copia! |
Poiché ora l’oggetto da copiare è const
, si tratta di una corrispondenza esatta per
il parametro del costruttore per copia. Il costruttore
template-izzato può essere istanziato in modo da avere la stessa
signature,
class Person
{ |
|
public: |
|
explicit Person(const Persons n); |
// istanziato
dal |
//
template |
|
Person(const
Person& rhs); |
// costr. per
copia |
// (generato dal
compilatore) |
|
… |
|
}; |
ma questo non conta, poiché una delle regole di risoluzione degli overloading in C++ è che nelle situazioni in cui un’istanziazione a template e una funzione non-template (ovvero una normale funzione) rappresentano corrispondenze ugualmente buone per una chiamata a funzione, dev’essere preferita la funzione “normale”. Il costruttore per copia (una funzione normale) batte pertanto un template istanziato con la stessa signature.
Se vi state chiedendo perché i compilatori generino un costruttore per copia quando potrebbero istanziare un costruttore template-izzato per ottenere la signature che avrebbe il costruttore per copia, rileggete l’Elemento 17.
L’interazione fra i costruttori perfect-forward e le operazioni di copia e spostamento generate dal compilatore si complica ulteriormente quando entra in gioco l’ereditarietà. In particolare, le implementazioni convenzionali delle operazioni di copia e spostamento per le classi derivate hanno comportamenti piuttosto sorprendenti.
class SpecialPerson:
public Person { |
|
public: |
|
SpecialPerson(const
SpecialPerson& rhs) |
// costr. per copia;
richiama |
: Person(rhs) |
// il costr.
perfect-forward |
{ ... } |
// della classe
base! |
SpecialPerson(SpecialPerson&& rhs) |
// costr. per
spostamento; richiama |
: Person(std::move(rhs)) |
// il costr.
perfect-forward |
{ ... } |
// della classe
base! |
}; |
Come indicato nel commento, i costruttori per
copia e per spostamento della classe derivata non richiamano i
costruttori per copia e per spostamento della classe base, bensì il
costruttore perfect-forward della classe base! Per comprendere il
perché, notate che le funzioni delle classi derivate stanno usando
argomenti di tipo SpecialPerson,
da
passare alla loro classe base, quindi sono sensibili alle
conseguenze dell’istanziazione template e della risoluzione
dell’overloading dei costruttori della classe Person.
Alla fine, il codice non verrà compilato,
poiché non esiste un costruttore per std::string
che accetta una SpecialPerson.
Spero, con questo, di avervi convinto che l’overloading per parametri che sono riferimenti universali dovrebbe essere evitato il più possibile. Ma se l’overloading per riferimenti universali è una cattiva idea, cosa fare quando occorre una funzione che inoltri la maggior parte dei tipi di argomenti, ma bisogna comunque trattare alcuni tipi di argomenti in modo particolare? Il problema può essere risolto in vari modi. Talmente tanti che gli abbiamo dedicato l’intero Elemento 27. Dunque vi basta continuare a leggere.
Argomenti da ricordare
• L’overloading per riferimenti universali quasi sempre fa sì che questo venga richiamato più frequentemente del dovuto.
• I costruttori perfect-forward
sono particolarmente problematici, poiché, in genere, sono
corrispondenze migliori rispetto ai costruttori per copia per gli
lvalue non-const
e possono attirare le chiamate dalla
classe derivata ai costruttori per copia e spostamento della classe
base.
Elemento 27 – Alternative all’overloading per riferimenti universali
L’Elemento 26 spiega che l’overloading per riferimenti universali può causare vari problemi, sia per le funzioni indipendenti sia per le funzioni membro (in particolare i costruttori). Inoltre, vi trovate esempi in cui tale overloading potrebbe essere utile. Se solo si comportasse nel modo che vorremmo! Questo Elemento esplora i modi per ottenere il comportamento desiderato, o tramite meccanismi che evitano di ricorrere all’overloading per riferimenti universali o impiegandoli in modi che vincolano i tipi di argomenti cui essi rispondono.
La discussione che segue sviluppa gli esempi introdotti nell’Elemento 26. Se non lo avete letto recentemente, può essere il caso di ripassarlo, prima di procedere.
Abbandonare l’overloading
Il primo esempio dell’Elemento 26,
logAndAdd,
è rappresentativo delle
molte funzioni che aiutano ad aggirare i problemi dell’overloading
per riferimenti universali utilizzando nomi differenti per quelli
che dovrebbero essere overloading. I due overloading di
logAndAdd,
per esempio, possono
essere suddivisi in logAndAddName
e
logAndAddNameIdx.
Purtroppo, questo
approccio non funziona per il secondo esempio che abbiamo
considerato, il costruttore di Person,
poiché il nome del costruttore viene
corretto dal linguaggio. Ma, a parte questo, perché rinunciare
completamente all’overloading?
Passaggio per const T&
Un’alternativa consiste nel tornare al C++98 e
sostituire i passaggi per riferimento universale con altrettanti
passaggi per riferimento lvalue a const.
In realtà, questo è il primo approccio
considerato nell’Elemento 26 (vedi a pagina 162). Il difetto di
questo approccio è che il risultato non è efficiente come vorremmo.
Sapendo ciò che sappiamo sull’interazione fra i riferimenti
universali e l’overloading, rinunciare a un po’ di efficienza per
guadagnare in termini di semplicità può essere un’alternativa meno
disdicevole di quanto possa sembrare.
Passaggio per valore
Un approccio che spesso consente di migliorare
le prestazioni senza aumentare la complessità consiste nel
sostituire, in modo assolutamente non intuitivo, i parametri a
passaggio per riferimento con altrettanti passaggi per valore.
Questa tecnica recepisce il consiglio fornito nell’Elemento 41 di
considerare il passaggio di oggetti per valore quando si sa che
essi verranno copiati; dunque si rimanda a tale Elemento per una
discussione dettagliata del funzionamento di questo meccanismo e
della sua efficienza. Qui vedremo solo come utilizzare la tecnica
nell’esempio di Person
:
class Person
{ |
|
public: |
|
explicit
Person(std::string n) |
// sostituisce il
costr. di T&& ctor; |
: name(std::move(n))
{} |
// vedi Elemento 41 per l’uso di
std::move |
explicit Person(int
idx) |
// come
prima |
:
name(nameFromIdx(idx)) {} |
|
… | |
private: |
|
std::string
name; |
|
}; |
Poiché non vi è alcun costruttore di
std::string
che accetta solo un
intero, tutti gli argomenti int
(o
comunque di tipo simile a int
) di un
costruttore di Person
(ovvero
std::size_t, short, long)
vengono
incanalati nell’overloading per int.
Analogamente, tutti gli argomenti di tipo std::string
(e tutto ciò che da std::string
può essere creato, come le stringhe
letterali come “Ruth”)
vengono
passati al costruttore che accetta una std::string.
Dunque nessuna sorpresa per i
chiamanti. Potete però pensare, immagino, che qualcuno possa
sorprendersi del fatto che utilizzando 0
o NULL
per
indicare un puntatore nullo si richiamerebbe l’overloading per
int;
ma, a questo punto, dovreste
consultare l’Elemento 8 e continuare a leggerlo finché non
fugherete ogni dubbio sull’uso di 0
o
NULL come puntatori nulli.
L’approccio tag dispatch
Né il passaggio per riferimento lvalue a
const
né il passaggio per valore
offrono il supporto per il perfect-forward. Se la motivazione per
ricorrere a un riferimento universale è il perfect-forward,
dobbiamo utilizzare un riferimento universale, non abbiamo altra
scelta. Tuttavia non vogliamo abbandonare del tutto l’overloading.
Pertanto, se non vogliamo rinunciare all’overloading e neppure ai
riferimenti universali, come possiamo evitare l’overloading sui
riferimenti universali?
In realtà non è così difficile. Le chiamate alle funzioni in overloading vengono risolte osservando tutti i parametri di tutti gli overloading, così come tutti gli argomenti nel punto della chiamata, scegliendo poi la funzione che rappresenta la corrispondenza migliore (tenendo in considerazione tutte le combinazioni parametro/argomento). Un parametro riferimento universale, generalmente, fornisce una corrispondenza esatta per tutto ciò che gli viene passato, ma se il riferimento universale fa parte di un elenco di parametri che contiene altri parametri che non sono riferimenti universali, alcune corrispondenze sufficientemente imprecise sui parametri che non sono riferimenti universali possono evitare che venga impiegato un overloading per riferimenti universali. Questo è ciò su cui si basa l’approccio tag dispatch. Un esempio aiuterà a comprendere la descrizione che segue.
Applicheremo la tecnica tag dispatch
all’esempio logAndAdd
presentato a
pagina 162. Ecco il codice di tale esempio:
std::multiset<std::string> names; |
// struttura dati
globale |
template<typename
T> |
// crea la voce log e
aggiunge |
void
logAndAdd(T&& name) |
// name alla
struttura dati |
{ |
|
auto now
= std::chrono::system clock::now(); |
|
log(now,
“logAndAdd”); |
|
names.emplace(std::forward<T>(name)); |
|
} |
|
Di per
se stessa, questa funzione si comporta bene, ma dovendo introdurre
l’overload che accetta un int
utilizzato per ricercare gli oggetti per indice, ci ritroviamo alla
situazione problematica dell’Elemento 26.
L’obiettivo di questo Elemento consiste proprio nell’evitarla.
Invece di aggiungere l’overload, reimplementeremo logAndAdd
per delegare il lavoro ad altre due
funzioni: una per i valori interi e una per tutto il resto.
logAndAdd
stessa accetterà tutti i
tipi di argomenti, sia interi sia di altro tipo.
Le due funzioni che svolgono il lavoro vero e
proprio si chiameranno logAndAddImpl,
ovvero utilizzeremo l’overloading. Una delle funzioni accetterà un
riferimento universale. Pertanto, avremo sia l’overloading sia i
riferimenti universali. Ma entrambe le funzioni accetteranno anche
un secondo parametro, che indica se l’argomento passato è di tipo
intero. Questo secondo parametro è ciò che ci impedirà di
precipitare nel problema della “voracità” descritto nell’Elemento 26,
poiché faremo in modo che il secondo parametro determini quale
overload selezionare.
Mi sembra di sentirvi brontolare: “Bla, bla, bla... Smettila di blaterare e mostraci il codice!”. Nessun problema. Ecco una versione quasi-corretta di logAndAdd:
template<typename
T> |
|
void
logAndAdd(T&& name) |
|
{ |
|
logAndAddImpl(std::forward<T>(name), |
|
std::is integral<T>()); |
// non
correttissima |
} |
Questa funzione inoltra il proprio parametro a
logAndAddImpl,
ma passa anche un
argomento che indica se il tipo di tale parametro (T
) è intero. Quanto meno, questo è ciò che
dovrebbe accadere. Per gli argomenti interi che sono rvalue, è
anche ciò che succede. Ma, come descritto nell’Elemento 28,
se al riferimento universale name
viene passato un argomento lvalue, il tipo dedotto per T
sarà un riferimento lvalue. Pertanto, se a
logAndAdd
viene passato un lvalue di
tipo int, T
verrà dedotto come
int&.
Questo non è un tipo
intero, poiché i riferimenti non sono interi. Ciò significa che
std::is_integral<T>
sarà
false
per ogni argomento lvalue,
anche se, in effetti, l’argomento rappresenta un valore intero.
Il fatto di aver individuato il problema ci
consente di trovare una soluzione, poiché la sempre disponibile
Libreria Standard del C++ ha un type trait (vedi Elemento 9),
std ::remove_reference,
che fa ciò
che suggerisce il nome, ossia quello di cui abbiamo bisogno:
rimuovere da un tipo ogni qualificatore di riferimento. Il modo
corretto per scrivere logAndAdd
è
pertanto:
template<typename
T>
void
logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename
std::remove_reference<T>::type>()
);
}
Questa
forma è finalmente corretta (il C++14, è più compatto: si puà usare
std::remove_reference_t<T>
al
posto del testo evidenziato; per i dettagli, consultate l’Elemento
9).
Tenendo conto di tutto questo, possiamo
rivolgere l’attenzione alla funzione chiamata, logAndAddImpl.
Vi sono due overload e il primo è
applicabile solo ai tipi non interi (ovvero ai tipi per i quali
std::is_integral<typename
std::remove_reference<T>::type>
dà false
):
template<typename
T> |
//
non-integral |
void
logAndAddImpl(T&& name, std::false_type) |
//
argomento: |
{ |
// lo
aggiunge |
auto now =
std::chrono::system_clock::now(); |
// alla struttura
dati |
log(now,
“logAndAdd”); |
//
globale |
names.emplace(std::forward<T>(name)); |
|
} |
Si tratta di codice piuttosto semplice, una
volta compresi i meccanismi su cui si basa il parametro
evidenziato. Teoricamente, logAndAdd
passa un valore booleano a logAndAddImpl,
indicando se a logAndAdd,
è stato passato il tipo intero, ma
true
e false
sono valori runtime
e noi dobbiamo utilizzare una risoluzione degli overload (un’attività di compilazione) per scegliere
l’overload corretto di logAndAddImpl.
Ciò significa che abbiamo bisogno di un tipo che corrisponda a
true
e di un tipo differente che
corrisponda a false.
Questa esigenza
è talmente comune che la Libreria Standard fornisce ciò di cui c’è
bisogno attraverso i nomi std::true_type
e std::false_type.
L’argomento passato a
logAndAddImpl
da logAndAdd
è un oggetto di un tipo che eredita da
std::true_type
se T
è intero e da std::false_type
se T
non è intero. Il risultato pratico è che questo
overload di logAndAddImpl
è un
candidato utilizzabile per la chiamata in logAndAdd
solo se T
non è un tipo intero.
Il secondo overload copre il caso opposto:
quando T
è un tipo intero. In tal
caso, logAndAddImpl
trova
semplicemente il nome corrispondente all’indice passato e poi passa
il nome a logAndAdd:
std::string
nameFromIdx(int idx); |
// vedi Elemento
26 |
void
logAndAddImpl(int idx, std::true_type) |
//
argomento |
{ |
// intero:
cerca |
logAndAdd(nameFromIdx(idx)); |
// il nome
e |
} |
//
richiamalogAndAdd |
// con tale
nome |
Facendo in modo che logAndAddImpl
per un indice ricerchi il nome
corrispondente e lo passi a logAndAdd
(da cui verrà inoltrato con std::forward
all’altro overload di logAndAddImpl
), evitiamo la necessità di inserire
il codice di log in entrambi gli overload di logAndAddImpl.
Utilizzando questa tecnica, i tipi
std::true_type
e std::false_type
sono semplici “tag”, il cui unico
scopo è quello di forzare la risoluzione dell’overloading in modo
che proceda esattamente come
desideriamo. Notate che non assegniamo neppure un nome a questi
parametri. Non hanno alcuno scopo runtime e, in realtà, speriamo
che i compilatori riconosceranno che i parametri tag non vengono
utilizzati e li ottimizzeranno escludendoli dall’immagine
d’esecuzione del programma (alcuni compilatori lo fanno, ma non
sempre). La chiamata alle funzioni di implementazione
dell’overloading all’interno di logAndAdd,
distribuisce (dispatch) il lavoro
all’overload corretto, facendo sì che venga creato l’oggetto tag
corretto. Da qui il nome di questa tecnica: tag
dispatch. Si tratta di un elemento costitutivo standard della
metaprogrammazione a template e più osserverete il codice delle
librerie C++ di oggi, più lo incontrerete.
Per i nostri scopi, ciò che è importante
relativamente alla tecnica tag dispatch non è tanto come funziona,
quanto come essa permetta di combinare riferimenti universali e
overloading, eliminando il problema descritto nell’Elemento 26.
La funzione di dispatch, logAndAdd,
accetta un parametro riferimento universale non vincolato, ma
questa funzione non subisce overloading. Le funzioni di
implementazione, logAndAddImpl,
subiscono invece overloading e una di esse accetta un parametro che
è un riferimento universale, ma la risoluzione delle chiamate di
queste funzioni dipende non solo dal parametro riferimento
universale, ma anche dal parametro tag. E i valori del tag sono
progettati in modo che un solo overload sia una corrispondenza
utilizzabile. Il risultato è che è il tag a determinare quale
overload richiamare. A questo punto, il fatto che il parametro
riferimento universale generi sempre una corrispondenza esatta per
il proprio argomento diviene ininfluente.