Wann fließen Typinformationen in C ++ rückwärts?

92

Ich habe gerade gesehen, wie Stephan T. Lavavej CppCon 2018über "Class Template Argument Deduction" sprach , wo er irgendwann übrigens sagt:

In C ++ fließen Informationen fast nie rückwärts ... Ich musste "fast" sagen, weil es ein oder zwei Fälle gibt, möglicherweise mehr, aber nur sehr wenige .

Obwohl ich versuchte herauszufinden, auf welche Fälle er sich beziehen könnte, konnte ich mir nichts einfallen lassen. Daher die Frage:

In welchen Fällen schreibt der C ++ 17-Standard vor, dass sich Typinformationen rückwärts verbreiten?

Massimiliano
quelle
Mustervergleich Teilspezialisierung und Destrukturierungszuweisungen.
v.oddou

Antworten:

80

Hier ist mindestens ein Fall:

struct foo {
  template<class T>
  operator T() const {
    std::cout << sizeof(T) << "\n";
    return {};
  }
};

Wenn Sie dies tun foo f; int x = f; double y = f;, fließen die Typinformationen "rückwärts", um herauszufinden, was drin Tist operator T.

Sie können dies auf eine fortgeschrittenere Weise verwenden:

template<class T>
struct tag_t {using type=T;};

template<class F>
struct deduce_return_t {
  F f;
  template<class T>
  operator T()&&{ return std::forward<F>(f)(tag_t<T>{}); }
};
template<class F>
deduce_return_t(F&&)->deduce_return_t<F>;

template<class...Args>
auto construct_from( Args&&... args ) {
  return deduce_return_t{ [&](auto ret){
    using R=typename decltype(ret)::type;
    return R{ std::forward<Args>(args)... };
  }};
}

also jetzt kann ich tun

std::vector<int> v = construct_from( 1, 2, 3 );

und es funktioniert.

Natürlich, warum nicht einfach {1,2,3}? Nun, {1,2,3}ist kein Ausdruck.

std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );

die zugegebenermaßen etwas mehr Zauberei erfordern: Live-Beispiel . (Ich muss die Ableitungsrückgabe veranlassen, eine SFINAE-Prüfung von F durchzuführen, dann die F SFINAE-freundlich machen, und ich muss std :: initializer_list im deduce_return_t-Operator T blockieren.)

Yakk - Adam Nevraumont
quelle
Sehr interessante Antwort, und ich habe einen neuen Trick gelernt, also vielen Dank! Ich musste eine Richtlinie zum Abzug von Vorlagen hinzufügen, damit Ihr Beispiel kompiliert wird , aber ansonsten funktioniert es wie ein Zauber!
Massimiliano
5
Das &&Qualifikationsspiel auf der operator T()ist eine tolle Sache; Es hilft, die schlechte Interaktion mit zu vermeiden, autoindem es einen Kompilierungsfehler verursacht, wenn autoes hier missbraucht wird.
Justin
1
Das ist sehr beeindruckend. Können Sie mich auf einen Hinweis verweisen / auf die Idee im Beispiel sprechen? oder vielleicht ist es original :) ...
llllllllll
3
@lili Welche Idee? Ich zähle 5: Verwenden Sie den Operator T, um Rückgabetypen abzuleiten? Verwenden Sie Tags, um den abgeleiteten Typ an ein Lambda zu übergeben? Verwenden Sie Konvertierungsoperatoren, um Ihre eigene Platzierungsobjektkonstruktion zu erstellen? Alle 4 verbinden?
Yakk - Adam Nevraumont
1
@lili Das Beispiel "Fortgeschrittener" ist, wie gesagt, nur 4 oder so zusammengeklebte Ideen. Ich habe das Kleben im laufenden Betrieb für diesen Beitrag durchgeführt, aber ich habe sicherlich viele Paare oder sogar Drillinge von denen gesehen, die zusammen verwendet wurden. Es ist eine Reihe von einigermaßen obskuren Techniken (wie Tootsie sich beschwert), aber nichts Neues.
Yakk - Adam Nevraumont
31

Stephan T. Lavavej erklärte den Fall, über den er sprach, in einem Tweet :

Der Fall, an den ich gedacht habe, ist, wo Sie die Adresse einer überladenen / vorlagengesteuerten Funktion verwenden können und wenn sie zum Initialisieren einer Variablen eines bestimmten Typs verwendet wird, wird dadurch eindeutig, welche Sie möchten. (Es gibt eine Liste, was eindeutig ist.)

Beispiele hierfür finden Sie auf der cppreference-Seite unter Adresse der überladenen Funktion . Ich habe einige davon unten ausgenommen:

int f(int) { return 1; } 
int f(double) { return 2; }   

void g( int(&f1)(int), int(*f2)(double) ) {}

int main(){
    g(f, f); // selects int f(int) for the 1st argument
             // and int f(double) for the second

     auto foo = []() -> int (*)(int) {
        return f; // selects int f(int)
    }; 

    auto p = static_cast<int(*)(int)>(f); // selects int f(int)
}

Michael Park fügt hinzu :

Es ist auch nicht auf die Initialisierung eines konkreten Typs beschränkt. Es könnte auch nur aus der Anzahl der Argumente schließen

und bietet dieses Live-Beispiel :

void overload(int, int) {}
void overload(int, int, int) {}

template <typename T1, typename T2,
          typename A1, typename A2>
void f(void (*)(T1, T2), A1&&, A2&&) {}

template <typename T1, typename T2, typename T3,
          typename A1, typename A2, typename A3>
void f(void (*)(T1, T2, T3), A1&&, A2&&, A3&&) {}

int main () {
  f(&overload, 1, 2);
}

was ich hier etwas näher erläutere .

Shafik Yaghmour
quelle
4
Wir könnten dies auch beschreiben als: Fälle, in denen die Art eines Ausdrucks vom Kontext abhängt?
MM
20

Ich glaube beim statischen Gießen überlasteter Funktionen geht der Fluss in die entgegengesetzte Richtung wie bei der üblichen Überlastauflösung. Eines davon ist also rückwärts, denke ich.

jbapple
quelle
7
Ich glaube das ist richtig. Und es ist, wenn Sie einen Funktionsnamen an einen Funktionszeigertyp übergeben; Typinformationen fließen vom Kontext des Ausdrucks (dem Typ, den Sie / constructing / etc zuweisen) rückwärts in den Namen der Funktion, um zu bestimmen, welche Überladung ausgewählt wird.
Yakk - Adam Nevraumont