Warum verhält sich diese Vorlagenfunktion nicht wie erwartet?

23

Ich habe über Vorlagenfunktionen gelesen und war durch dieses Problem verwirrt:

#include <iostream>

void f(int) {
    std::cout << "f(int)\n";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << "  ";
    f(val);
}

void f(double) {
    std::cout << "f(double)\n";
}

template void g<double>(double);

int main() {
    f(1.0); // f(double)
    f(1);   // f(int)
    g(1.0); // d  f(int), this is surprising
    g(1);   // i  f(int)
}

Die Ergebnisse sind die gleichen, wenn ich nicht schreibe template void g<double>(double);.

Ich denke, g<double>sollte danach instanziiert werden f(double), und daher sollte der Anruf bei fin ganrufen f(double). Überraschenderweise ruft es immer noch f(int)an g<double>. Kann mir jemand helfen, das zu verstehen?


Nachdem ich die Antworten gelesen hatte, fand ich heraus, was meine Verwirrung wirklich ist.

Hier ist ein aktualisiertes Beispiel. Es ist größtenteils unverändert, außer dass ich eine Spezialisierung hinzugefügt habe für g<double>:

#include <iostream>

void f(int){cout << "f(int)" << endl;}

template<typename T>
void g(T val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

void f(double){cout << "f(double)" << endl;}

//Now use user specialization to replace
//template void g<double>(double);

template<>
void g<double>(double val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

int main() {
    f(1.0); // f(double)
    f(1);  // f(int)
    g(1.0); // now d  f(double)
    g(1);  // i  f(int)
}

Verhält sich mit der Benutzerspezialisierung g(1.0)wie erwartet.

Sollte der Compiler diese Instanziierung nicht automatisch g<double>an derselben Stelle main()ausführen (oder sogar danach , wie in Abschnitt 26.3.3 der C ++ - Programmiersprache , 4. Ausgabe beschrieben)?

Zhongqi Cheng
quelle
3
Der letzte Anruf g(1)gibt i f(int)für mich. Du hast geschrieben d f(double). War das ein Tippfehler?
HTNW
Ja. Es tut uns leid. aktualisiert
Zhongqi Cheng
Das Grundprinzip der Vorlage besteht darin, die Verwendung von Operationen für Benutzertypen zu unterstützen und gleichzeitig die Entführung interner Bibliotheksaufrufe durch vom Benutzer deklarierte Symbole zu verhindern. Dies ist ein unmöglicher Kompromiss, da es keine "Konzept" -Verträge für Vorlagen gibt und es zu spät ist, solche soliden "Verträge" einzuführen.
Neugieriger

Antworten:

12

Der Name fist ein abhängiger Name (er hängt Tüber das Argument ab val) und wird in zwei Schritten aufgelöst :

  1. Bei der Nicht-ADL-Suche werden Funktionsdeklarationen untersucht, die im Kontext der Vorlagendefinition sichtbar sind .
  2. ADL untersucht Funktionsdeklarationen ..., die entweder im Kontext der Vorlagendefinition oder im Kontext der Vorlageninstanziierung sichtbar sind .

void f(double)ist im Kontext der Vorlagendefinition nicht sichtbar, und ADL findet es auch nicht, weil

Bei Argumenten vom grundlegenden Typ ist der zugehörige Satz von Namespaces und Klassen leer


Wir können Ihr Beispiel leicht ändern:

struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}

Jetzt wird ADL void f(Double)im zweiten Schritt finden und die Ausgabe wird sein 6Double f(Double). Wir können ADL deaktivieren, indem wir statt (f)(val)(oder ::f(val)) schreiben f(val). Dann wird die Ausgabe 6Double f(Int)in Übereinstimmung mit Ihrem Beispiel sein.

Evg
quelle
Vielen Dank. Ich frage mich, wo sich die Instanziierung für g <double> im Code befindet. Ist es kurz vor main (). Wenn ja, sollte die instanziierte g <double> -Definition nicht in der Lage sein, sowohl f (int) als auch f (double) zu sehen und schließlich f (double) auszuwählen?
Zhongqi Cheng
@ZhongqiCheng In Schritt 1 nur die Vorlage Definition Kontext betrachtet werden, und aus diesem Zusammenhang void f(double)ist nicht sichtbar - dieser Zusammenhang endet vor seiner Erklärung. In Schritt 2 ADL wird nichts finden, so dass die Vorlage Instanziierung Kontext hier keine Rolle spielen.
Evg
@ZhongqiCheng, in Ihrer Bearbeitung haben Sie nachher eine Definition eingeführt void f(double), sodass diese Funktion von dort aus sichtbar ist. Jetzt fist kein abhängiger Name. Wenn f(val);nach der Definition von eine bessere Übereinstimmung für deklariert wurde g<double>, wird diese ebenfalls nicht gefunden. Die einzige Möglichkeit, nach vorne zu schauen, ist ADL (oder ein alter Compiler, der die zweiphasige Suche nicht korrekt implementiert).
Evg
Hier ist mein Verständnis Ihrer Antwort. Ich sollte annehmen, dass die Funktionsvorlagen (g <int> und g <double>) direkt nach der Vorlagendefinition instanziiert werden. Daher wird f (double) nicht angezeigt. Ist das richtig. Ich danke dir sehr.
Zhongqi Cheng
@ ZhongqiCheng, kurz zuvor instanziiert main(). Sie werden es nicht sehen f(double), denn wenn die Instanziierung stattfindet, ist es zu spät: Phase eins der Suche wurde bereits durchgeführt und es wurde keine gefunden f(double).
Evg
6

Das Problem f(double)wurde an der Stelle, an der Sie es aufrufen, noch nicht deklariert. Wenn Sie die Deklaration vor das verschieben template g, wird sie aufgerufen.

Bearbeiten: Warum sollte man manuelle Instanziierung verwenden?

(Ich werde nur über Funktionsvorlagen sprechen, analoge Argumentation gilt auch für Klassenvorlagen.) Die Hauptanwendung besteht darin, die Kompilierungszeiten zu verkürzen und / oder den Code der Vorlage vor Benutzern zu verbergen.

C ++ - Programme werden in zwei Schritten in Binärdateien integriert: Kompilieren und Verknüpfen. Damit die Kompilierung eines Funktionsaufrufs erfolgreich ist, wird nur der Header der Funktion benötigt. Damit die Verknüpfung erfolgreich ist, wird eine Objektdatei benötigt, die den kompilierten Hauptteil der Funktion enthält.

Wenn der Compiler nun einen Aufruf einer Vorlagenfunktion sieht, hängt seine Funktion davon ab, ob er den Hauptteil der Vorlage oder nur den Header kennt. Wenn nur der Header angezeigt wird, geschieht dasselbe wie wenn die Funktion nicht als Vorlage verwendet wurde: Informationen zum Aufruf des Linkers werden in die Objektdatei eingefügt. Wenn es aber auch den Hauptteil der Vorlage sieht, macht es noch etwas anderes: Es instanziiert die richtige Instanz des Hauptteils, kompiliert diesen Körper und fügt ihn ebenfalls in die Objektdatei ein.

Wenn mehrere Quelldateien dieselbe Instanz der Vorlagenfunktion aufrufen, enthält jede ihrer Objektdateien eine kompilierte Version der Instanz der Funktion. (Linker weiß davon und löst alle Aufrufe einer einzelnen kompilierten Funktion auf, sodass nur eine in der endgültigen Binärdatei des Programms / der Bibliothek vorhanden ist.) Um jedoch jede der Quelldateien zu kompilieren, musste die Funktion instanziiert werden und kompiliert, was einige Zeit in Anspruch nahm.

Es reicht aus, wenn der Linker seine Arbeit erledigt, wenn sich der Hauptteil der Funktion in einer Objektdatei befindet. Das manuelle Instanziieren der Vorlage in einer Quelldatei ist eine Möglichkeit, den Compiler dazu zu bringen, den Hauptteil der Funktion in die Objektdatei der betreffenden Quelldatei einzufügen. (Es ist ein bisschen so, als ob die Funktion aufgerufen würde, aber die Instanziierung wird an einer Stelle geschrieben, an der ein Funktionsaufruf ungültig wäre.) Wenn dies erledigt ist, können alle Dateien, die Ihre Funktion aufrufen, kompiliert werden, wobei nur der Header der Funktion bekannt ist Dies spart Zeit, um den Funktionskörper bei jedem Aufruf zu instanziieren und zu kompilieren.

Der zweite Grund (Ausblenden der Implementierung) könnte jetzt sinnvoll sein. Wenn eine Bibliotheksautorin möchte, dass Benutzer ihrer Vorlagenfunktion die Funktion verwenden können, gibt sie ihnen normalerweise den Code der Vorlage, damit sie ihn selbst kompilieren können. Wenn sie den Quellcode der Vorlage geheim halten wollte, konnte sie die Vorlage in dem Code, den sie zum Erstellen der Bibliothek verwendet, manuell instanziieren und den Benutzern die so erhaltene Objektversion anstelle der Quelle geben.

Ist das sinnvoll?

AshleyWilkes
quelle
Ich wäre Ihnen dankbar, wenn Sie den Unterschied zwischen der im ersten Code des Autors dargestellten Instanziierung und der Spezialisierung im zweiten Code des Autors nach der Bearbeitung erklären könnten. Ich habe die Website von cppreference über Spezialisierung und Instanziierung und Bücher oft gelesen, aber ich habe sie nicht verstanden. Vielen Dank
Dev
@ Dev: Bitte geben Sie Ihre Frage etwas genauer an. Ich bin mir nicht sicher, was ich beantworten soll. Grundsätzlich besteht in diesem Fall der Unterschied darin, dass der Compiler sie verwendet, wenn die Spezialisierung vorhanden ist. Wenn sie nicht vorhanden ist, nimmt der Compiler die Vorlage, generiert eine Instanz davon und verwendet diese generierte Instanz. Im obigen Code führen sowohl die Spezialisierung als auch die Instanz der Vorlage zu demselben Code.
AshleyWilkes
Meine Frage bezieht sich genau auf diesen Teil des Codes: "template void g <double> (double);" In der Programmiervorlage heißt es Instanziierung, wenn Sie das wissen. Die Spezialisierung ist etwas anders, da sie wie im zweiten Code deklariert ist, den der Autor gesendet hat: "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Könnten Sie mir den Unterschied erklären?
Dev
@Dev Ich habe bereits versucht, dies zu tun: Der Compiler verwendet eine Spezialisierung, wenn dies möglich ist. Wenn die Spezialisierung nicht angezeigt wird (z. B. weil keine vorhanden ist), erstellt der Compiler eine Instanz der Vorlage und verwendet diese Instanz. Im obigen Code führen sowohl die Vorlage als auch die Spezialisierung zum gleichen Ergebnis. Der einzige Unterschied besteht darin, was der Compiler tut, um zu diesem Ergebnis zu gelangen. In anderen Fällen kann die Spezialisierung eine beliebige Implementierung enthalten. Sie muss nichts mit der Vorlage gemeinsam haben (außer für den Methodenheader). Klarer?
AshleyWilkes
1
Die template void g<double>(double);sogenannte manuelle Instanziierung (beachten Sie die templateohne spitze Klammern, das ist ein Unterscheidungsmerkmal der Syntax); Dadurch wird der Compiler angewiesen, eine Instanz der Methode zu erstellen. Hier hat es wenig Wirkung, wenn es nicht da wäre, würde der Compiler die Instanz an der Stelle generieren, an der die Instanz aufgerufen wird. Die manuelle Instanziierung wird selten verwendet. Ich werde sagen, warum Sie sie möglicherweise verwenden möchten, nachdem Sie bestätigt haben, dass die Sache jetzt klarer ist :-)
AshleyWilkes