Schlechter Typabzug beim Übergeben eines überladenen Funktionszeigers und seiner Argumente

8

Ich versuche, einen Wrapper bereitzustellen, um std::invokeden Funktionstyp auch dann abzuleiten, wenn die Funktion überlastet ist.
(Ich habe gestern eine verwandte Frage für die Variadic- und Methodenzeigerversion gestellt).

Wenn die Funktion ein Argument hat, funktioniert dieser Code (C ++ 17) unter normalen Überlastbedingungen wie erwartet:

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

Das Reduzieren des Code-Aufblähens ist offensichtlich für mehr Eingabeargumente erforderlich, aber das ist ein separates Problem.

Wenn der Benutzer Überladungen hat, die sich nur durch const / reference unterscheiden, wie folgt:

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

Dann Invokeleitet daraus die Funktion falsch mit g ++ Ausgang:

(int &)
(const int &)

(int &&)
(const int &)

Und clang ++:

(int &)
(const int &)

(int &&)
(int &&)

(Danke an geza für den Hinweis, dass die Ergebnisse von clang unterschiedlich waren).

Hat Invokealso undefiniertes Verhalten.

Ich vermute, dass Metaprogrammierung der Weg wäre, um dieses Problem anzugehen. Ist es unabhängig davon möglich, den Typabzug vor Ort korrekt zu handhaben Invoke?

Elliott
quelle
Was ist die erwartete Leistung? Ist es (int &) (int &&)?
LF
@LF, ja. Dies sind die Ausgaben von Foo, daher sollten sie auch die Ausgabe von Invoke sein.
Elliott
1
Für mich ergibt Clang ein anderes Ergebnis: Es wird (int &&)zweimal für den zweiten Fall gedruckt .
Geza
2
Es muss etwas mit SArgumentenabzug zu tun haben . Versuchen Sie, die const T &Version von zu kommentieren Invokeund den Fehler zu bemerken. Auch wenn das Argument explizit ( Invoke<void>(&Foo, num)) angegeben wird, wird die richtige Version aufgerufen.
Aparpara
2
Hier ist eine Theorie für den ersten Fall: Wenn der Compiler die Nicht-Konstante berücksichtigt Invoke, kann er sie sowohl mit der Konstante als auch mit der Nicht-Konstante instanziieren Foo. Und es wird nicht überprüft, ob der Rückgabetyp ( S) für beide gleich ist, sodass nicht abgeleitet werden kann S. Daher wird diese Vorlage ignoriert. Während der Instanziierung Invokekann die Konstante nur mit der Konstante ausgeführt werden Foo, sodass Sin diesem Fall abgeleitet werden kann. Daher verwendet der Compiler diese Vorlage.
Geza

Antworten:

1

Theorie

Bei jeder Funktionsvorlage Invokewird bei der Ableitung des Vorlagenarguments (die erfolgreich sein muss, damit die Überlastungsauflösung berücksichtigt wird) jeweils Foo geprüft, ob für den einen Funktionsparameter ( func) so viele Vorlagenparameter (hier zwei) abgeleitet werden können . Der Gesamtabzug kann nur erfolgreich sein, wenn genau einer Fooübereinstimmt (da sonst kein Abzug möglich ist S). (Dies wurde mehr oder weniger in den Kommentaren angegeben.)

Der erste ("nach Wert") Invokeüberlebt nie: Er kann aus jedem der Foos ableiten . In ähnlicher Weise constakzeptiert die zweite ("Nichtreferenz ") Überlastung die ersten beiden Foos. Beachten Sie, dass diese unabhängig vom anderen Argument für Invoke(for arg) gelten!

Die dritte ( const T&) Überlastung wählt die entsprechende FooÜberlastung aus und leitet T= ab int; Der letzte macht dasselbe mit der letzten Überladung (wo T&&es sich um eine normale r-Wert-Referenz handelt) und lehnt daher l-Wert-Argumente trotz seiner universellen Referenz ab (die in diesem Fall Tals int&(oder const int&) ableitet und mit funcdem Abzug in Konflikt steht ).

Compiler

Wenn das Argument für argein r-Wert ist (und wie üblich nicht const ist), können beide plausiblen InvokeÜberladungen erfolgreich abgeleitet werden, und die T&&Überladung sollte gewinnen (da sie einen r-Wert-Verweis an einen r- Wert bindet ).

Für den Fall aus den Kommentaren:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

Es erfolgt kein Abzug von, &Barda es sich um eine Funktionsvorlage handelt, und wird daher in jedem Fall Terfolgreich (as int) abgeleitet . Dann Abzug geschieht wieder für jeden Fall die identifizieren BarSpezialisierung (falls vorhanden) zu verwenden, herzuleiten , Uwie scheitern , int&, const int&, und int&jeweils. Die int&Fälle sind identisch und eindeutig besser, daher ist der Anruf nicht eindeutig.

Also ist Clang genau hier. (Aber hier gibt es kein "undefiniertes Verhalten".)

Lösung

Ich habe keine allgemeine Antwort für Sie; Da bestimmte Parametertypen mehrere Wertekategorie / Konstanten-Qualifizierungspaare akzeptieren können, ist es in all diesen Fällen nicht einfach, die Überlastungsauflösung korrekt zu emulieren. Es gab Vorschläge, Überlastungssätze auf die eine oder andere Weise zu korrigieren. Sie könnten eine der aktuellen Techniken in dieser Richtung in Betracht ziehen (wie ein generisches Lambda pro Zielfunktionsname).

Davis Herring
quelle