Warum nimmt std :: function nicht an der Überlastungsauflösung teil?

17

Ich weiß, dass der folgende Code nicht kompiliert werden kann.

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;
}

Weil std::functiongesagt wird, Überlastungsauflösung nicht zu berücksichtigen, wie ich in diesem anderen Beitrag gelesen habe .

Ich verstehe die technischen Einschränkungen, die diese Art von Lösung erzwungen haben, nicht vollständig.

Ich habe über die Phasen der Übersetzung und Vorlagen auf cppreference gelesen, aber ich kann mir keine Argumentation vorstellen, für die ich kein Gegenbeispiel finden konnte. Was und in welcher Übersetzungsphase lässt sich das oben Gesagte nicht kompilieren, wenn es einem halben Laien erklärt wird (noch neu in C ++)?

TuRtoise
quelle
1
@Evg, aber es gibt nur eine Überladung, die sinnvoll wäre, um im OP-Beispiel akzeptiert zu werden. In Ihrem Beispiel würden beide ein Match machen
idclev 463035818

Antworten:

13

Dies hat eigentlich nichts mit "Übersetzungsphasen" zu tun. Es geht nur um die Konstrukteure von std::function.

Siehe, std::function<R(Args)>erfordert nicht, dass die angegebene Funktion genau vom Typ ist R(Args). Insbesondere ist es nicht erforderlich, dass ihm ein Funktionszeiger gegeben wird. Es kann einen beliebigen aufrufbaren Typ ( operator()Elementfunktionszeiger , ein Objekt mit einer Überladung von ) annehmen, solange es aufgerufen werden kann, als ob es ArgsParameter verwendet und etwas konvertierbares zurückgibt R(oder wenn dies der Fall Rist void, kann es alles zurückgeben).

Dazu muss der entsprechende Konstruktor von std::functioneine Vorlage sein : template<typename F> function(F f);. Das heißt, es kann jeder Funktionstyp verwendet werden (vorbehaltlich der oben genannten Einschränkungen).

Der Ausdruck bazrepräsentiert eine Überlastmenge. Wenn Sie diesen Ausdruck verwenden, um den Überlastungssatz aufzurufen, ist das in Ordnung. Wenn Sie diesen Ausdruck als Parameter für eine Funktion verwenden, die einen bestimmten Funktionszeiger verwendet, kann C ++ die Überlastmenge auf einen einzelnen Aufruf reduzieren und so in Ordnung bringen.

Sobald eine Funktion jedoch eine Vorlage ist und Sie die Ableitung von Vorlagenargumenten verwenden, um herauszufinden, um welchen Parameter es sich handelt, kann C ++ die korrekte Überladung im Überlastungssatz nicht mehr ermitteln. Sie müssen es also direkt angeben.

Nicol Bolas
quelle
Entschuldigen Sie, wenn ich etwas falsch gemacht habe, aber warum kann C ++ nicht zwischen verfügbaren Überladungen unterscheiden? Ich sehe jetzt, dass std :: function <T> jeden kompatiblen Typ akzeptiert, nicht nur eine exakte Übereinstimmung, sondern nur eine dieser beiden baz () - Überladungen, die aufgerufen werden kann, als ob sie bestimmte Parameter angenommen hätte. Warum ist es unmöglich zu unterscheiden?
TuRtoise
Weil alles, was C ++ sieht, die von mir zitierte Signatur ist. Es weiß nicht, gegen was es passen soll. Im Wesentlichen zwingt Sie die Sprache jedes Mal, wenn Sie einen Überlastungssatz für etwas verwenden, bei dem nicht klar ist, welche Antwort explizit aus dem C ++ - Code stammt (der Code ist diese Vorlagendeklaration), zu formulieren, was Sie meinen.
Nicol Bolas
1
@TuRtoise: Der Vorlagenparameter in der functionKlassenvorlage ist irrelevant . Was zählt, ist der Vorlagenparameter auf dem Konstruktor, den Sie aufrufen. Welches ist nur typename F: aka, jeder Typ.
Nicol Bolas
1
@ TuRtoise: Ich denke du missverstehst etwas. Es ist "nur F", weil Vorlagen so funktionieren. Aus der Signatur nimmt dieser Funktionskonstruktor einen beliebigen Typ, sodass bei jedem Versuch, ihn mit Abzug von Vorlagenargumenten aufzurufen, der Typ aus dem Parameter abgeleitet wird. Jeder Versuch, einen Abzug auf einen Überlastungssatz anzuwenden, ist ein Kompilierungsfehler. Der Compiler versucht nicht, jeden möglichen Typ aus dem Satz abzuleiten, um zu sehen, welcher funktioniert.
Nicol Bolas
1
"Jeder Versuch, einen Abzug auf eine Überlastungsmenge anzuwenden, ist ein Kompilierungsfehler. Der Compiler versucht nicht, jeden möglichen Typ aus der Menge abzuleiten, um zu sehen, welche funktionieren." Genau das, was mir gefehlt hat, danke :)
TuRtoise
7

Eine Überlastungsauflösung tritt nur auf, wenn (a) Sie den Namen einer Funktion / eines Operators aufrufen oder (b) ihn in einen Zeiger (auf eine Funktion oder eine Elementfunktion) mit einer expliziten Signatur umwandeln.

Beides tritt hier nicht auf.

std::functionNimmt jedes Objekt, das mit seiner Signatur kompatibel ist . Es wird kein spezieller Funktionszeiger benötigt. (Ein Lambda ist keine Standardfunktion, und eine Standardfunktion ist kein Lambda.)

In meinen Homebrew-Funktionsvarianten R(Args...)akzeptiere ich zur Signatur R(*)(Args...)aus genau diesem Grund auch ein Argument (eine genaue Übereinstimmung). Dies bedeutet jedoch, dass die Signaturen mit "exakter Übereinstimmung" über die "kompatiblen" Signaturen angehoben werden.

Das Hauptproblem besteht darin, dass ein Überlastungssatz kein C ++ - Objekt ist. Sie können einen Überlastungssatz benennen, ihn jedoch nicht "nativ" weitergeben.

Jetzt können Sie einen Pseudo-Überlastungssatz einer Funktion wie folgt erstellen:

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

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

Dadurch wird ein einzelnes C ++ - Objekt erstellt, das eine Überlastungsauflösung für einen Funktionsnamen durchführen kann.

Wenn wir die Makros erweitern, erhalten wir:

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

das ist nervig zu schreiben. Eine einfachere, nur etwas weniger nützliche Version finden Sie hier:

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

Wir haben ein Lambda, das eine beliebige Anzahl von Argumenten akzeptiert und diese dann perfekt weiterleitet baz .

Dann:

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

funktioniert. Wir verschieben die Überlastungsauflösung auf das Lambda, in dem wir speichern fun, anstatt zu übergebenfun einen Überlastungssatz direkt zu übergeben (den er nicht auflösen kann).

Es gab mindestens einen Vorschlag, eine Operation in der C ++ - Sprache zu definieren, die einen Funktionsnamen in ein Überlastungssatzobjekt konvertiert. Bis ein solcher Standardvorschlag im Standard enthalten ist, ist das OVERLOADS_OFMakro nützlich.

Sie könnten noch einen Schritt weiter gehen und den Zeiger auf einen kompatiblen Funktionszeiger unterstützen.

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)...); };
  }
};

aber das wird langsam stumpf.

Live Beispiel .

#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)...); }; \
    } \
  }
Yakk - Adam Nevraumont
quelle
5

Das Problem hier ist, dass dem Compiler nichts sagt, wie er die Funktion zum Zeigerzerfall ausführen soll. Wenn Sie haben

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;
}

Dann würde Code funktionieren, da der Compiler jetzt weiß, welche Funktion Sie möchten, da Sie einen konkreten Typ zuweisen.

Wenn Sie verwenden std::function, rufen Sie den Funktionsobjektkonstruktor auf, der die Form hat

template< class F >
function( F f );

und da es sich um eine Vorlage handelt, muss der Typ des übergebenen Objekts abgeleitet werden. Da bazes sich um eine überladene Funktion handelt, kann kein einzelner Typ abgeleitet werden, sodass der Vorlagenabzug fehlschlägt und Sie eine Fehlermeldung erhalten. Sie müssten verwenden

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

einen einzelnen Typ erzwingen und den Abzug zulassen.

NathanOliver
quelle
"Da baz eine überladene Funktion ist, kann kein einzelner Typ abgeleitet werden", aber seit C ++ 14 "nimmt dieser Konstruktor nicht an der Überladungsauflösung teil, es sei denn, f ist für Argumenttypen Args ... und Rückgabetyp R" I am Ich bin mir nicht sicher, aber ich war kurz
davor
3
@ ehemalsknownas_463035818 Um dies festzustellen, muss zuerst ein Typ abgeleitet werden, und dies ist nicht möglich, da es sich um einen überladenen Namen handelt.
NathanOliver
1

Zu diesem Zeitpunkt entscheidet der Compiler, welche Überladung an den std::functionKonstruktor übergeben werden soll. Er weiß lediglich, dass der std::functionKonstruktor für einen beliebigen Typ vorgesehen ist. Es ist nicht in der Lage, beide Überladungen zu versuchen und festzustellen, dass die erste nicht kompiliert wird, die zweite jedoch.

Die Möglichkeit, dies zu beheben, besteht darin, dem Compiler explizit mitzuteilen, welche Überladung Sie mit einem static_cast:

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