Perché std :: function non partecipa alla risoluzione del sovraccarico?


17

So che il seguente codice non verrà compilato.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Perché std::functionsi dice che non consideri la risoluzione del sovraccarico, come ho letto in questo altro post .

Non capisco appieno i limiti tecnici che hanno costretto questo tipo di soluzione.

Ho letto delle fasi della traduzione e dei modelli su cppreference, ma non riesco a pensare a nessun ragionamento a cui non sono riuscito a trovare un controesempio. Spiegato a un mezzo laico (ancora nuovo in C ++), cosa e durante quale fase della traduzione non riesce a compilare quanto sopra?


1
@Evg ma c'è solo un sovraccarico che avrebbe senso essere accettato nell'esempio dei PO. Nel tuo esempio entrambi farebbero una partita
idclev 463035818,

Risposte:


13

Questo non ha nulla a che fare con le "fasi della traduzione". Riguarda esclusivamente i costruttori distd::function .

Vedi, std::function<R(Args)>non richiede che la data funzione sia esattamente del tipo R(Args). In particolare, non richiede che gli venga assegnato un puntatore a funzione. Può richiedere qualsiasi tipo richiamabile (puntatore alla funzione membro, qualche oggetto che ha un sovraccarico di operator()) purché sia ​​invocabile come se avesse preso Argsparametri e restituisse qualcosa convertibile in R (o se Rèvoid , può restituire qualsiasi cosa).

Per fare ciò, il costruttore appropriato di std::functiondeve essere un modello :template<typename F> function(F f); . Cioè, può assumere qualsiasi tipo di funzione (soggetto alle restrizioni di cui sopra).

L'espressione bazrappresenta un set di sovraccarico. Se usi quell'espressione per chiamare il set di overload, va bene. Se si utilizza quell'espressione come parametro per una funzione che accetta un puntatore a funzione specifica, C ++ può ridurre il sovraccarico impostato a una singola chiamata, rendendolo quindi perfetto.

Tuttavia, una volta che una funzione è un modello e si utilizza la deduzione dell'argomento modello per capire quale sia quel parametro, C ++ non è più in grado di determinare quale sia il sovraccarico corretto nel set di sovraccarico. Quindi è necessario specificarlo direttamente.


Scusami se ho sbagliato qualcosa, ma come mai il C ++ non ha la capacità di distinguere tra i sovraccarichi disponibili? Vedo ora che std :: function <T> accetta qualsiasi tipo compatibile, non solo una corrispondenza esatta, ma solo uno di quei due sovraccarichi baz () è invocabile come se avesse preso parametri specifici. Perché è impossibile disambiguare?
TuRtoise,

Perché tutto il C ++ vede è la firma che ho citato. Non sa a cosa dovrebbe corrispondere. In sostanza, ogni volta che usi un set di sovraccarico rispetto a qualcosa che non è ovvio quale sia la risposta giusta esplicitamente dal codice C ++ (il codice è quella dichiarazione del modello), il linguaggio ti costringe a precisare cosa intendi.
Nicol Bolas,

1
@TuRtoise: il parametro template sul functiontemplate di classe è irrilevante . Ciò che conta è il parametro template sul costruttore che stai chiamando. Che è solo typename F: aka, qualsiasi tipo.
Nicol Bolas,

1
@TuRtoise: penso che tu abbia frainteso qualcosa. È "solo F" perché è così che funzionano i modelli. Dalla firma, quel costruttore di funzioni accetta qualsiasi tipo, quindi qualsiasi tentativo di chiamarlo con deduzione dell'argomento template dedurrà il tipo dal parametro. Qualsiasi tentativo di applicare la detrazione a un set di sovraccarico è un errore di compilazione. Il compilatore non tenta di dedurre ogni tipo possibile dall'insieme per vedere quali funzionano.
Nicol Bolas,

1
"Qualsiasi tentativo di applicare la detrazione a un set di sovraccarico è un errore di compilazione. Il compilatore non tenta di dedurre ogni tipo possibile dall'insieme per vedere quali funzionano." Esattamente quello che mi mancava, grazie :)
TuRtoise,

7

La risoluzione del sovraccarico si verifica solo quando (a) si chiama il nome di una funzione / operatore o (b) lo si lancia su un puntatore (a funzione o funzione membro) con una firma esplicita.

Né si sta verificando qui.

std::functionaccetta qualsiasi oggetto compatibile con la sua firma. Non richiede specificamente un puntatore a funzione. (una lambda non è una funzione std e una funzione std non è una lambda)

Ora nelle mie varianti di funzione homebrew, per la firma R(Args...)accetto anche un R(*)(Args...)argomento (una corrispondenza esatta) proprio per questo motivo. Ma significa che eleva le firme di "corrispondenza esatta" al di sopra delle firme "compatibili".

Il problema principale è che un set di overload non è un oggetto C ++. È possibile nominare un set di overload, ma non è possibile passarlo in giro "nativamente".

Ora puoi creare un set di pseudo-overload di una funzione come questa:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

questo crea un singolo oggetto C ++ che può sovraccaricare la risoluzione su un nome di funzione.

Espandendo le macro, otteniamo:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

che è fastidioso da scrivere. Una versione più semplice, solo leggermente meno utile, è qui:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

abbiamo una lambda che accetta un numero qualsiasi di argomenti, quindi li inoltra perfettamente a baz.

Poi:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

lavori. Ritardiamo la risoluzione del sovraccarico nel lambda in cui archiviamo fun, invece di passare fundirettamente un set di sovraccarico (che non può risolvere).

C'è stata almeno una proposta per definire un'operazione nel linguaggio C ++ che converte un nome di funzione in un oggetto set di sovraccarico. Fino a quando una proposta del genere non è nello standard, la OVERLOADS_OFmacro è utile.

Potresti fare un ulteriore passo avanti e supportare il cast-to-compatibile-pointer-funzione.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

ma questo sta iniziando a diventare ottuso.

Esempio dal vivo .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }

5

Il problema qui è che non c'è nulla che dice al compilatore come eseguire la funzione per il decadimento del puntatore. Se hai

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Quindi il codice funzionerebbe poiché il compilatore sa quale funzione si desidera in quanto esiste un tipo concreto a cui si sta assegnando.

Quando lo usi std::functionchiami è il costruttore di oggetti funzione che ha il modulo

template< class F >
function( F f );

e poiché è un modello, deve dedurre il tipo di oggetto che viene passato. poiché bazè una funzione sovraccarica non esiste un singolo tipo che può essere dedotto, quindi la deduzione del modello fallisce e si ottiene un errore. Dovresti usare

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

per forzare un singolo tipo e consentire la detrazione.


"poiché baz è una funzione sovraccaricata non esiste un singolo tipo che può essere dedotto" ma dal C ++ 14 "questo costruttore non partecipa alla risoluzione del sovraccarico a meno che f non sia Callable per i tipi di argomento Args ... e restituisca il tipo R" Sono non sono sicuro, ma ero vicino ad aspettarmi che questo sarebbe bastato per risolvere l'ambiguità
idclev 463035818

3
@ formerlyknownas_463035818 Ma per determinarlo, deve prima dedurre un tipo e non può, poiché è un nome sovraccarico.
NathanOliver,

1

Nel momento in cui il compilatore sta decidendo quale sovraccarico passare nel std::functioncostruttore tutto ciò che sa è che il std::functioncostruttore è tentato di prendere qualsiasi tipo. Non ha la possibilità di provare entrambi i sovraccarichi e di scoprire che il primo non si compila ma il secondo lo fa.

Il modo per risolverlo è dire esplicitamente al compilatore quale sovraccarico si desidera con un static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
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.