Чому ця функція шаблону не веде себе так, як очікувалося?


23

Я читав про функції шаблону і заплутався у цій проблемі:

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

Результати однакові, якщо я не пишу template void g<double>(double);.

Я думаю, g<double>що після цього слід створити екземпляри f(double), і тому заклик до fвходу gповинен закликати f(double). Дивно, але до цих пір викликає f(int)в g<double>. Хтось може мені допомогти зрозуміти це?


Прочитавши відповіді, я зрозумів, в чому насправді моя плутанина.

Ось оновлений приклад. Це здебільшого без змін, за винятком того, що я додав спеціалізацію для 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)
}

Зі спеціалізацією користувача g(1.0)поводиться так, як я очікував.

Чи не повинен компілятор автоматично робити те саме , що описується g<double>там же (або навіть після main(), як описано в розділі 26.3.3 мови програмування C ++ , 4-е видання)?


3
Останній дзвінок g(1), дає i f(int)для мене. Ти написав d f(double). Це була друкарня?
HTNW

так. вибачте. оновлено
Zhongqi Cheng

Основний принцип шаблону полягає у підтримці використання операцій над типами користувачів, незважаючи на те, щоб запобігти захопленню внутрішніх викликів бібліотеки за оголошеними користувачем символами. Це неможливий компроміс, оскільки немає «концептуальних» контрактів на шаблони, і вводити такі надійні «контракти» вже пізно.
curiousguy

Відповіді:


12

Ім'я f- це залежне ім'я (це залежить від Tаргументу val), і воно буде вирішено в два етапи :

  1. Пошук без ADL розглядає декларації функцій ..., які видно з контексту визначення шаблону .
  2. ADL вивчає декларації функцій ..., які видно або з контексту визначення шаблону, або з контексту інстанції шаблону .

void f(double)не видно з контексту визначення шаблону, і ADL його також не знайде, тому що

Для аргументів основного типу пов’язаний набір просторів імен та класів порожній


Ми можемо трохи змінити ваш приклад:

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

Тепер ADL знайдеться void f(Double)на другому кроці, і вихід буде 6Double f(Double). Ми можемо відключити ADL, написавши (f)(val)(або ::f(val)) замість f(val). Тоді результат буде 6Double f(Int), згідно з вашим прикладом.


Дуже дякую. Мені цікаво, де в коді є примірник для g <double>. Це безпосередньо перед main (). Якщо так, то чи не повинен вміщений g <double> визначення бачити як f (int), так і f (double) і, нарешті, вибирати f (double)?
Чжунчі Чен

@ZhongqiCheng На кроці 1 буде розглянуто лише контекст визначення шаблону , і з цього контексту void f(double)не видно - цей контекст закінчується до його оголошення. На кроці 2 ADL нічого не знайде, тому контекст опису шаблону тут не грає ніякої ролі.
Evg

@ZhongqiCheng, у редагуванні ви ввели визначення після void f(double), тому ця функція видно з нього. Тепер fце не залежне ім'я. Якщо було б краще відповідати f(val);заявленому після визначення g<double>, воно також не знайдеться. Єдиний спосіб "заглянути вперед" - ADL (або якийсь старий компілятор, який не здійснює двофазний пошук правильно).
Evg

Ось моє розуміння вашої відповіді. Я повинен припустити, що шаблони функцій (g <int> і g <double>) створюються миттєво відразу після визначення шаблону. Тому він не побачить f (подвійний). Це правильно. Дуже дякую.
Zhongqi Cheng

@ZhongqiCheng, попередньо створений раніше main(). Вони не побачать f(double), бо коли відбувається інстанціяція, то вже пізно: перша фаза пошуку вже зроблена, і вона не знайшла "ні" f(double).
Evg

6

Проблема f(double)не була оголошена в тому місці, де ви її називаєте; якщо ви перемістите його декларацію перед template g, воно буде викликано.

Редагувати: Навіщо використовувати ручну інстанцію?

(Я розповім лише про шаблони функцій, аналогічна аргументація має місце і для шаблонів класів.) Основне використання полягає у скороченні часу компіляції та / або приховуванні коду шаблону від користувачів.

Програма C ++ вбудована у двійкові файли в 2 етапи: компіляція та зв'язування. Для компіляції функціонального дзвінка потрібен лише заголовок функції. Для того, щоб зв'язок вдався, потрібен об’єктний файл, що містить скомпільоване тіло функції.

Тепер, коли компілятор бачить виклик шаблонованої функції, від того, що він робить, залежить те, чи знає тіло шаблону або лише заголовок. Якщо він бачить лише заголовок, він робить те саме, що якби функція не була шаблонна: ставить інформацію про виклик посилання на об'єктний файл. Але якщо він також бачить тіло шаблону, він також робить інше: він створює власний екземпляр тіла, компілює це тіло і також вкладає його в файл об'єктів.

Якщо кілька вихідних файлів викликають один і той же екземпляр шаблонованої функції, кожен їх об'єктний файл буде містити скомпільовану версію екземпляра функції. (Linker знає про це і вирішує всі виклики до однієї компільованої функції, тому в остаточному бінарному файлі програми / бібліотеки буде лише один.) Однак для складання кожного з вихідних файлів функція повинна була бути ініційована і складено, на що знадобився час.

Досить, щоб лінкер виконав цю роботу, якщо тіло функції знаходиться в одному об’єктному файлі. Вручну інстанціювати шаблон у вихідному файлі - це спосіб змусити компілятор помістити тіло функції в об'єктний файл відповідного вихідного файлу. (Це якось так, як якщо б функція була викликана, але інстанція записується там, де виклик функції був би недійсним.) Коли це зроблено, всі файли, які викликають вашу функцію, можуть бути зібрані, знаючи лише заголовок функції, таким чином заощадити час, що знадобиться для інстанціювання та складання тіла функції при кожному з викликів.

Друга причина (приховування реалізації) може мати сенс зараз. Якщо автор бібліотеки хоче, щоб користувачі її функції шаблону могли використовувати цю функцію, вона зазвичай надає їм код шаблону, щоб вони могли скласти його самостійно. Якщо вона хотіла б зберегти вихідний код шаблону в секреті, вона могла б вручну інстанціювати шаблон у коді, який він використовує для створення бібліотеки та надання користувачам отриманої таким чином об’єкта замість джерела.

Це має сенс?


Я буду вдячний, якщо ви зможете пояснити різницю між примірником, представленим у першому коді автора, та спеціалізацією у другому коді автора після редагування. Я багато разів читав сайт cppreference про спеціалізацію та інстанції та книги, але не зрозумів. Спасибі
Dev

@Dev: Будь ласка, вкажіть своє запитання трохи більше, я не впевнений, що відповісти. В основному в цьому випадку різниця полягає в тому, що коли спеціалізація присутня, компілятор використовує її, а коли її немає, компілятор приймає шаблон, генерує його екземпляр і використовує цей створений екземпляр. У наведеному вище коді і спеціалізація, і екземпляр шаблону призводять до одного і того ж коду.
AshleyWilkes

Моє запитання полягає саме в тій частині коду: "шаблон void g <double> (подвійний);" Його називають примірником у шаблоні програмування, якщо ви це знаєте. Спеціалізація дещо інша, тому що вона оголошена як у другому коді, автор надіслав "шаблон <> void g <double> (подвійний val) {cout << typeid (val) .name () <<" "; f ( val);} "Чи можете ви пояснити мені різницю?
Дев

@Dev Я вже намагався це зробити: компілятор використовує спеціалізацію, якщо може; якщо він не може побачити спеціалізацію (наприклад, тому що її немає), компілятор створює екземпляр шаблону і використовує цей екземпляр. У наведеному вище коді і шаблон, і спеціалізація призводять до однакового результату, тому різниця полягає лише в тому, що робить компілятор, щоб досягти цього результату. В інших випадках спеціалізація може містити будь-яку реалізацію, вона не повинна мати нічого спільного з шаблоном (але для заголовка методу). Ясніше?
AshleyWilkes

1
template void g<double>(double);Так званий ручний конкретизації (зауважте template, без кутових дужок, це відмінна риса синтаксису); що повідомляє компілятору створити екземпляр методу. Тут це мало ефекту, якби його не було, компілятор генерував би екземпляр у тому місці, де викликається екземпляр. Уручну інстанціювання рідко використовується функцією, я скажу, чому ви можете скористатися нею після підтвердження речі, тепер зрозуміліше :-)
AshleyWilkes
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.