2016-07-11 53 views
21

Sono stato recentemente sorpreso dal fatto che lambda può essere assegnato a std::function s con leggermente diverso firme. Leggermente diverso significa che i valori di ritorno del lambda potrebbero essere ignorati quando il function viene specificato per restituire void o che i parametri potrebbero essere riferimenti nel valore function ma valori nel lambda.Quali sono le regole di conversione del tipo per i parametri e i valori di ritorno di lambdas?

Vedere this example (ideone) dove ho evidenziato ciò che sospetto essere incompatibile. Vorrei pensare che il valore di ritorno non è un problema perché si può sempre chiamare una funzione e ignorare il valore di ritorno, ma la conversione da un riferimento a un valore sembra strano per me:

int main() { 
    function<void(const int& i)> f; 
    //  ^^^^ ^^^^^ ^
    f = [](int i) -> int { cout<<i<<endl; return i; }; 
    //  ^^^ ^^^^^^ 
    f(2); 
    return 0; 
} 

La domanda è minore : Perché questo codice è compilato e funziona? La domanda principale è: quali sono le regole generali per la conversione di tipo dei parametri lambda e dei valori restituiti se usati insieme a std::function?

+0

Inoltre: esiste un termine specifico per ciò che sto descrivendo qui o è * tipo di conversione * corretto? – anderas

+0

Sembra un po 'come covarianza e controvarianza, ma penso che questi termini siano usati solo tra classi di base e derivate in OO. – Quentin

+0

Non ho il problema. Se hai una funzione con firma 'void (const int & i)', puoi inoltrare nel suo corpo una funzione con firma 'int (int)'? Sì. Non stai violando i qualificatori di cv (beh, stai copiando il valore, quindi ...) e puoi liberamente scartare il valore restituito. L'opposto sarebbe stato invece sbagliato e non funziona davvero. Quindi, qual è il problema attuale? – skypjack

risposta

16

È possibile assign a lambda to a function object di tipo std::function<R(ArgTypes...)> quando la lambda è Lvalue-Callable per quella firma. A sua volta, Lvalue-Callable è definito in termini di INVOKE operation, che qui significa che il lambda deve essere chiamabile quando è un lvalue e tutti i suoi argomenti sono valori dei tipi richiesti e categorie di valori (come se ogni argomento fosse il risultato di chiamare una funzione nulla con quel tipo di argomento come il tipo restituito nella sua firma).

Cioè, se si dà il lambda un id in modo che possiamo fare riferimento al suo tipo,

auto l = [](int i) -> int { cout<<i<<endl; return i; }; 

Per assegnare ad un function<void(const int&)>, l'espressione

static_cast<void>(std::declval<decltype(l)&>()(std::declval<const int&>())) 

deve essere ben formata.

Il risultato di std::declval<const int&>() è un riferimento lvalue a const int, ma non c'è alcuna obbligatorio che l'argomento int, poiché questo è solo il lvalue-to-rvalue conversion, che è considered an exact match ai fini della risoluzione sovraccarico problema:

return l(static_cast<int const&>(int{})); 

Come hai osservato, i valori di ritorno vengono scartati if the function object signature has return type void; in caso contrario, i tipi di reso devono essere implicitamente convertibili. Come sottolinea Jonathan Wakely, C++ 11 ha avuto un comportamento insoddisfacente su questo (Using `std::function<void(...)>` to call non-void function; Is it illegal to invoke a std::function<void(Args...)> under the standard?), ma è stato corretto da allora, in LWG 2420; la risoluzione è stata applicata come correzione post pubblicazione a C++ 14. Il più moderno compilatore C++ fornirà il comportamento C++ 14 (come modificato) come un'estensione, anche in modalità C++ 11.

+0

È [questo] (http://eel.is/c++draft/func.require#2) il link giusto per questo caso? – skypjack

+0

@skypjack è il caso speciale per i tipi di ritorno 'void', sì (dal momento che non è possibile convertire un valore in" void "). Ma penso che il PO sia più interessato al comportamento delle conversioni di argomenti e dove questi sono OK. – ecatmur

+0

Ha senso. Vorrei solo mettere in evidenza qual è il caso specifico, tutto qui. ;-) – skypjack

4

L'operatore di assegnazione è definito per avere l'effetto:

function(std::forward<F>(f)).swap(*this); 

(14882: 2011 20.8.11.2.1 par.18)

Il costruttore che questo riferimenti,

template <class F> function(F f); 

richiede:

F sarà CopyConstructible. f deve essere Callable (20.8.11.2) per il tipo di argomento ArgTypes e il tipo di ritorno R.

dove Callable è definito come segue:

Un oggetto richiamabile f di tipo F è Callable 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 formato (20.8.2).

INVOKE, a sua volta, è defined as:

  1. Definire INVOKE (f, t1, t2, ..., tN) come segue:

    • ... casi per la gestione delle funzioni membro omessi qui ...
    • f(t1, t2, ..., tN) in tutti gli altri casi .
  2. 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.

Poiché la definizione INVOKE diventa una chiamata di funzione normale, in questo caso, gli argomenti possono essere converted: Se il vostro std::function<void(const int&)> accetta un const int& allora può essere convertita in un int per la chiamata. L'esempio che segue riunisce con clang++ -std=c++14 -stdlib=libc++ -Wall -Wconversion:

int main() { 
    std::function<void(const int& i)> f; 
    f = [](int i) -> void { std::cout << i << std::endl; }; 
    f(2); 
    return 0; 
} 

il tipo di ritorno (void) è gestita dalla speciale caso static_cast<void> per la definizione INVOKE.

noti, tuttavia, che, al momento della scrittura seguente genera un errore quando si compila con clang++ -std=c++1z -stdlib=libc++ -Wconversion -Wall, ma non se compilato con clang++ -std=c++1z -stdlib=libstdc++ -Wconversion -Wall:

int main() { 
    std::function<void(const int& i)> f; 
    f = [](int i) -> int { std::cout << i << std::endl; return i;}; 
    f(2); 
    return 0; 
} 

Ciò è dovuto libc++ attuare il comportamento come specificato in C++ 14, anziché lo amended behaviour descritto sopra (grazie a @Jonathan Wakely per averlo indicato). @Arunmu descrive il tratto di tipo libstdc++ responsabile della stessa cosa in his post.A questo proposito, le implementazioni possono comportarsi in modo leggermente diverso quando si gestiscono le callebles con i tipi di ritorno void, a seconda che implementino C++ 11, 14 o qualcosa di più recente.

+0

Grazie per la risposta, mi piace molto il fatto che abbia citato lo standard, ma la risposta di @ ecatmur spiega le stesse cose in un modo leggermente più conciso senza sacrificare nessuno dei punti principali, quindi ho accettato quello. merita la stessa quantità di voti :-) – anderas

+1

Il comportamento di libC++ non è un bug, perché libC++ implementa la regola di C++ 14, non quella nella bozza di lavoro a cui sei collegato. La regola è cambiata con http: // cplusplus. github.io/LWG/lwg-defects.html#2420 –

+0

@JonathanWakely Grazie per aver segnalato questo aspetto. Ho modificato la risposta per riflettere questo. – Andrew

3

Basta aggiungere al @ecatmur risposta, g ++/libstd ++ solo ha scelto di ignorare il valore di ritorno del callable, è altrettanto normale, come si farebbe per ignorare il valore di ritorno nel codice normale:

static void 
_M_invoke(const _Any_data& __functor, _ArgTypes&&... __args) 
{ 
    (*_Base::_M_get_pointer(__functor))( // Gets the pointer to the callable 
      std::forward<_ArgTypes>(__args)...); 
} 

Il tipo caratteristica che consente in modo esplicito questo libstd ++ è:

template<typename _From, typename _To>  
using __check_func_return_type = __or_<is_void<_To>, is_convertible<_From, _To>>; 
4

la conversione da un riferimento a un valore sembra strano per me

Perché?

Anche questo sembra strano?

int foo(int i) { return i; } 

void bar(const int& ir) { foo(ir); } 

Questa è esattamente la stessa. Una funzione che prende un valore int viene chiamata da un'altra funzione, prendendo uno int per riferimento costante.

All'interno di bar viene copiata la variabile ir e il valore di ritorno viene ignorato. Questo è esattamente ciò che accade all'interno dello std::function<void(const int&)> quando ha una destinazione con la firma int(int).