2015-09-30 10 views
14

Ho problemi con std :: funzioni create da lambdas se la funzione restituisce un riferimento ma il tipo di ritorno non è esplicitamente chiamato come riferimento. Sembra che la funzione std :: sia stata creata correttamente senza avvertimenti, ma al momento della chiamata, viene restituito un valore quando è previsto un riferimento, causando l'esplosione. Ecco un esempio molto artificiosa:Comportamento anomalo di ritorno con std :: function creato da lambda (C++)

#include <iostream> 
#include <vector> 
#include <functional> 

int main(){ 
    std::vector<int> v; 
    v.push_back(123); 
    std::function<const std::vector<int>&(const std::vector<int>&)> callback = 
     [](const std::vector<int> &in){return in;}; 
    std::cout << callback(v).at(0) << std::endl; 
    return 0; 
} 

Questo stampa spazzatura, se il lambda viene modificato per restituire in modo esplicito riferimento a un const funziona benissimo. Riesco a capire il compilatore pensando che lambda è return-by-value senza il suggerimento (quando inizialmente mi sono imbattuto in questo problema, il lambda restituiva direttamente il risultato da una funzione che restituiva un riferimento const, nel qual caso pensavo che il il ritorno di riferimento const della lambda sarebbe deducibile, ma apparentemente no.) Quello che mi sorprende è che il compilatore consente di costruire la funzione std :: dal lambda con tipi di ritorno non corrispondenti. È previsto questo comportamento? Mi manca qualcosa nello standard che permetta questo disadattamento? Lo vedo con g ++ (GCC) 4.8.2, non l'ho provato con nient'altro.

Grazie!

+2

@Nawaz Perché hai eliminato la risposta? – Barry

+1

Clang ++ 3.7.0 stampa anche garbage (g ++ porta a un errore di segmentazione per me). – jhnnslschnr

+0

@jhnnslschnr Grazie per il controllo - non so perché la risposta postata è stata cancellata (sembrava corretta) ma l'essenza era che un lambda che restituisce un valore si legherà felicemente a una funzione std :: con un tipo di ritorno di riferimento, il risultato finale è che il lambda restituisce una copia e la funzione restituisce un riferimento alla copia temporanea. Suppongo che non sia diverso dal restituire una variabile locale da una funzione con un tipo di ritorno di riferimento. – Kevin

risposta

12

Perché è rotto?

Quando viene dedotto il tipo di ritorno di una lambda, le referenze e le qualifiche cv vengono eliminate. Quindi il tipo di ritorno di

[](const std::vector<int> &in){return in;}; 

è solo std::vector<int>, non std::vector<int> const&. Di conseguenza, se ci spogliamo la lambda e std::function parte del codice, abbiamo effettivamente abbiamo:

std::vector<int> lambda(std::vector<int> const& in) 
{ 
    return in; 
} 

std::vector<int> const& callback(std::vector<int> const& in) 
{ 
    return lambda(in); 
} 

lambda restituisce un temporaneo. In effetti è appena stato copiato il suo input. Questo temporaneo è vincolato al rendimento di riferimento in callback. Ma i temporaries legati a un riferimento in un'istruzione return non hanno una durata estesa, quindi il temporaneo viene distrutto alla fine dell'istruzione return. Pertanto, a questo punto:

callback(v).at(0) 
-----------^ 

abbiamo un riferimento penzoloni ad una distrutta copia di v.

La soluzione è quella di specificare in modo esplicito il tipo di ritorno del lambda ad essere un riferimento:

[](const std::vector<int> &in)-> const std::vector<int>& {return in;} 
[](const std::vector<int> &in)-> decltype(auto) {return in;} // C++14 

Ora non ci sono copie, nessun provvisori, senza riferimenti penzolanti, e non un comportamento indefinito.

Chi è la colpa?

Per quanto riguarda il comportamento previsto, la risposta è effettivamente sì. Le condizioni per costruibilità di un std::function sono [func.wrap.func.con]:

f è richiamabile (20.9.12.2) per tipi di argomento ArgTypes... e tipo R ritorno.

dove, [func.wrap.FUNC]:

Un oggetto richiamabile f di tipo F è richiamabile per tipi di argomento ArgTypes e di ritorno di tipo R se l'espressione INVOKE (f, declval<ArgTypes>()..., R), considerata come operando non valutata (punto 5), è ben formata (20.9 .2).

dove, [func.require], l'enfasi è mia:

Definire INVOKE(f, t1, t2, ..., tN, R) come static_cast<void>(INVOKE (f, t1, t2, ..., tN)) se R è cvvoid, altrimenti INVOKE(f, t1, t2, ..., tN)implicitamente convertito in R.

Quindi, se avessimo:

T func(); 
std::function<T const&()> wrapped(func); 

Che in realtà soddisfa tutti i requisiti standard: INVOKE(func) è ben formato, e mentre ritorna T, Tè implicitamente convertibile in T const&. Quindi questo non è un bug di gcc o clang. Questo è probabilmente un difetto standard, in quanto non vedo perché tu voglia mai permettere una tale costruzione. Questo sarà mai essere valido, quindi la formulazione dovrebbe probabilmente richiedere che se R è un tipo di riferimento quindi F deve restituire un tipo di riferimento pure.

+1

In C++ 14 può essere '-> decltype (auto)'. – 0x499602D2

+0

@ 0x499602D2 Buona cosa aggiungere, grazie. – Barry

+0

È vero per la deduzione del tipo, ma non hai risposto all'altra metà della domanda. Semplificato: perché 'funzione ' consentita di collegarsi a un lambda che restituisce un 'T' in base al valore? Questo * crea * sempre un riferimento ciondolante. Direi che si tratta di un bug della libreria o di una svista dello standard poiché questo caso può essere controllato in fase di compilazione usando 'decltype'. –

2

Ho fatto un po 'di ricerche personali per quanto riguarda il costruttore std::function. Sembra che questa parte sia una svista nell'interazione di std::function e dello standard Callable concept. std::function<R(Args...)>::function<F>(F) richiede F per essere Callable come R(Args...), che di per sé sembra ragionevole. Callable per R(Args...) richiede tipo di ritorno F s'(quando somministrato argomenti di tipi Args... per la conversione implicita R, che ha anche in sé sembra ragionevole. Ora, quando R è const R_ &, questo sarà un consente la conversione implicita di R_ per const R_ & perché i riferimenti const sono consentito di collegarsi ad rvalues. Questo non è necessariamente pericolosa. ad esempio, prendere in considerazione una funzione di f() che restituisce un int, ma è considerato richiamabile come const int &().

const int &result = f(); 
if (f == 5) 
{ 
    // ... 
} 

non v'è alcun problema qui a causa delle regole C++ s 'per l'estensione la vita di un temporaneo. comportamento Tuttavia, quanto segue è undefined:

std::function<const int &()> fWrapped{f}; 
if (fWrapped() == 5) 
{ 
    // ... 
} 

Questo perché durata di vita non si applica qui. Il temporaneo viene creato all'interno dello operator() e viene distrutto prima del confronto.

Pertanto, costruttore std::function 's probabilmente non dovrebbe fare affidamento su Callable solo, ma far rispettare l'ulteriore restrizione che la conversione implicita di un rvalue ad un const valore assegnabile al fine di impegnare ad un riferimento è vietato.In alternativa, è possibile modificare Callable per non consentire mai questa conversione, a scapito di non consentire un utilizzo sicuro (se non altro a causa dell'estensione della durata).

Per rendere le cose ancora più complicate, fWrapped() dell'esempio precedente è perfettamente sicuro da chiamare, a condizione che non si acceda al target del riferimento ciondolante.

Problemi correlati