Dlaczego ta funkcja szablonu nie działa zgodnie z oczekiwaniami?


23

Czytałem o funkcjach szablonów i ten problem mnie pomieszał:

#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)
}

Wyniki są takie same, jeśli nie piszę template void g<double>(double);.

Myślę, że g<double>powinien zostać f(double)utworzony później , a zatem wezwanie do fw gpowinno zadzwonić f(double). Zaskakująco, to nadal nazywa f(int)się g<double>. Czy ktoś może mi pomóc to zrozumieć?


Po przeczytaniu odpowiedzi zorientowałem się, jakie jest moje zamieszanie.

Oto zaktualizowany przykład. Jest to w większości niezmienione oprócz tego, że dodałem specjalizację dla 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)
}

Dzięki specjalizacji użytkownika g(1.0)zachowuje się tak , jak się spodziewałem.

Czy kompilator nie powinien automatycznie wykonywać tej samej instancji g<double>w tym samym miejscu (lub nawet później main(), jak opisano w sekcji 26.3.3 języka programowania C ++ , wydanie 4)?


3
Ostatnie połączenie g(1)daje i f(int)mi. Napisałeś d f(double). Czy to była literówka?
HTNW,

tak. Przepraszam. zaktualizowano
Zhongqi Cheng

Podstawową zasadą szablonu jest obsługa operacji na typach użytkowników, przy jednoczesnym zapobieganiu przejmowaniu wywołań biblioteki wewnętrznej przez symbole zadeklarowane przez użytkownika. Jest to niemożliwy kompromis, ponieważ nie ma umów „koncepcyjnych” na szablony i jest za późno na wprowadzenie takich „umów”.
ciekawy,

Odpowiedzi:


12

Nazwa fjest nazwą zależną (zależy od Targumentu val) i zostanie podzielona na dwa etapy :

  1. Wyszukiwanie inne niż ADL sprawdza deklaracje funkcji ... widoczne z kontekstu definicji szablonu .
  2. ADL sprawdza deklaracje funkcji ... widoczne z kontekstu definicji szablonu lub kontekstu tworzenia szablonu .

void f(double)nie jest widoczny z kontekstu definicji szablonu i ADL też go nie znajdzie, ponieważ

W przypadku argumentów typu podstawowego skojarzony zestaw przestrzeni nazw i klas jest pusty


Możemy nieznacznie zmodyfikować twój przykład:

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

Teraz ADL znajdzie void f(Double)w drugim kroku, a wynik będzie 6Double f(Double). Możemy wyłączyć ADL, pisząc (f)(val)(lub ::f(val)) zamiast f(val). Wtedy wynik będzie 6Double f(Int)zgodny z twoim przykładem.


Dziękuję Ci bardzo. Zastanawiam się, gdzie w kodzie znajduje się instancja dla g <double>. Czy to tuż przed main (). Jeśli tak, to czy instancja definicji g <double> nie powinna widzieć zarówno f ​​(int) i f (double), a na końcu wybrać f (double)?
Zhongqi Cheng

@ZhongqiCheng W kroku 1 brany będzie pod uwagę tylko kontekst definicji szablonu , az tego kontekstu void f(double)nie widać - kontekst ten kończy się przed deklaracją. W kroku 2 ADL niczego nie znajdzie, więc kontekst tworzenia szablonów nie odgrywa tutaj żadnej roli.
Evg

@ZhongqiCheng, w swojej edycji wprowadziłeś definicję po void f(double), więc ta funkcja jest z niej widoczna. Teraz fnie jest to nazwa zależna. Jeśli f(val);po zdefiniowaniu było lepsze dopasowanie do zadeklarowanego g<double>, również nie zostanie znalezione. Jedynym sposobem na „patrzenie w przyszłość” jest ADL (lub jakiś stary kompilator, który nie implementuje poprawnie wyszukiwania dwufazowego).
Evg

Oto moje rozumienie twojej odpowiedzi. Powinienem założyć, że szablony funkcji (g <int> ig <double>) są tworzone natychmiast po definicji szablonu. Dlatego nie zobaczy f (podwójne). Czy to jest poprawne. Dziękuję bardzo.
Zhongqi Cheng

@ZhongqiCheng, utworzony bezpośrednio przed main(). Nie zobaczą f(double), ponieważ gdy wystąpi tworzenie instancji, jest już za późno: faza pierwsza wyszukiwania została już wykonana i nie znaleziono f(double).
Evg

6

Problem f(double)nie został zadeklarowany w miejscu, w którym go nazwiesz; jeśli przeniesiesz jego deklarację przed template g, zostanie wywołane.

Edycja: Dlaczego warto stosować ręczne tworzenie instancji?

(Porozmawiam tylko o szablonach funkcji, analogiczna argumentacja dotyczy również szablonów klas.) Głównym zastosowaniem jest skrócenie czasu kompilacji i / lub ukrycie kodu szablonu przed użytkownikami.

Program C ++ jest wbudowany w pliki binarne w 2 krokach: kompilacji i łączenia. Aby kompilacja wywołania funkcji zakończyła się powodzeniem, potrzebny jest tylko nagłówek funkcji. Aby połączenie zakończyło się powodzeniem, potrzebny jest plik obiektowy zawierający skompilowany korpus funkcji.

Teraz, gdy kompilator widzi wywołanie funkcji szablonowej , to, co robi, zależy od tego, czy zna treść szablonu, czy tylko nagłówek. Jeśli widzi tylko nagłówek, robi to samo, jakby funkcja nie była szablonowana: umieszcza informacje o wywołaniu linkera do pliku obiektowego. Ale jeśli widzi również treść szablonu, robi również inną rzecz: tworzy odpowiednią instancję obiektu, kompiluje to ciało i umieszcza je również w pliku obiektowym.

Jeśli kilka plików źródłowych wywołuje to samo wystąpienie funkcji szablonowej, każdy z ich plików obiektowych będzie zawierał skompilowaną wersję wystąpienia funkcji. (Linker wie o tym i rozwiązuje wszystkie wywołania jednej skompilowanej funkcji, więc będzie tylko jedno w końcowym pliku binarnym programu / biblioteki.) Jednak aby skompilować każdy plik źródłowy, funkcja musiała zostać utworzona i skompilowane, co wymagało czasu.

Wystarczy, aby linker wykonał swoją pracę, jeśli treść funkcji znajduje się w jednym pliku obiektowym. Aby ręcznie utworzyć szablon w pliku źródłowym, można zmusić kompilator do umieszczenia treści funkcji w pliku obiektowym danego pliku źródłowego. (To trochę tak, jakby funkcja została wywołana, ale instancja jest zapisywana w miejscu, w którym wywołanie funkcji byłoby niepoprawne.) Po wykonaniu tej czynności wszystkie pliki, które wywołują twoją funkcję, można skompilować, znając tylko nagłówek funkcji, a zatem oszczędność czasu zajęłoby utworzenie i skompilowanie treści funkcji z każdym wywołaniem.

Drugi powód (ukrywanie implementacji) może mieć teraz sens. Jeśli autor biblioteki chce, aby użytkownicy jej funkcji szablonu mogli korzystać z tej funkcji, zwykle przekazuje im kod szablonu, aby mogli sami go skompilować. Jeśli chciała zachować kod źródłowy szablonu w tajemnicy, mogłaby ręcznie utworzyć instancję szablonu w kodzie, którego używa do budowy biblioteki, i dać użytkownikom wersję obiektu uzyskaną w ten sposób zamiast źródła.

Czy to ma jakiś sens?


Będę wdzięczny za wyjaśnienie różnicy między instancją przedstawioną w pierwszym kodzie autora a specjalizacją w drugim kodzie autora po edycji. Wielokrotnie czytałem stronę dotyczącą preferencji na temat specjalizacji i tworzenia instancji oraz książek, ale nie rozumiałem. Dziękuję
Dev

@Dev: Proszę podać swoje pytanie, nie jestem pewien, na co odpowiedzieć. Zasadniczo w tym przypadku różnica polega na tym, że gdy specjalizacja jest obecna, kompilator z niej korzysta, a gdy nie jest obecny, kompilator pobiera szablon, generuje jego instancję i używa tej wygenerowanej instancji. W powyższym kodzie zarówno specjalizacja, jak i instancja szablonu prowadzą do tego samego kodu.
AshleyWilkes,

Moje pytanie dotyczy dokładnie tej części kodu: „template void g <double> (double);” W szablonie programistycznym nosi nazwę instancji, jeśli o tym wiesz. Specjalizacja jest nieco inna, ponieważ została zadeklarowana jak w drugim kodzie, autor wysłał „szablon <> void g <double> (double val) {cout << typeid (val) .name () <<”; f ( val);} "Czy możesz mi wyjaśnić różnicę?
Dev

@Dev Próbowałem już to zrobić: kompilator używa specjalizacji, jeśli to możliwe; jeśli nie widzi specjalizacji (np. ponieważ jej nie ma), kompilator tworzy instancję szablonu i używa tej instancji. W powyższym kodzie zarówno szablon, jak i specjalizacja prowadzą do tego samego wyniku, więc jedyną różnicą jest to, co kompilator robi, aby uzyskać ten wynik. W innych przypadkach specjalizacja może zawierać dowolną implementację, nie musi ona mieć nic wspólnego z szablonem (ale dla nagłówka metody). Jaśniejsze?
AshleyWilkes,

1
Jest template void g<double>(double);to tak zwana instancja ręczna (zwróć uwagę na templatebrak nawiasów ostrych, to cecha wyróżniająca składnię); która informuje kompilator o utworzeniu instancji metody. Tutaj ma to niewielki wpływ, gdyby go nie było, kompilator wygenerowałby instancję w miejscu jej wywołania. Ręczna instancja jest rzadko używana, powiem dlaczego możesz chcieć jej użyć po tym, jak potwierdzisz, że sprawa jest teraz jaśniejsza :-)
AshleyWilkes,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.