Почему эта функция шаблона работает не так, как ожидалось?

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

#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-е издание)?

2 ответа

Имя 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), в соответствии с вашим примером.

Проблема в f(double)не был объявлен в том месте, где вы его называете; если вы поместите его объявление передtemplate g, он будет вызван.

Изменить: зачем использовать ручное создание экземпляров?

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

Программа на C++ встраивается в двоичные файлы в 2 этапа: компиляция и компоновка. Для успешной компиляции вызова функции нужен только заголовок функции. Для успешного связывания необходим объектный файл, содержащий скомпилированное тело функции.

Теперь, когда компилятор видит вызов шаблонной функции, то, что он делает, зависит от того, знает ли он тело шаблона или только заголовок. Если он видит только заголовок, он делает то же самое, как если бы функция не была шаблонизирована: помещает информацию о вызове компоновщика в объектный файл. Но если он также видит тело шаблона, он делает еще одну вещь: он создает соответствующий экземпляр тела, компилирует это тело и также помещает его в объектный файл.

Если несколько исходных файлов вызывают один и тот же экземпляр шаблонной функции, каждый из их объектных файлов будет содержать скомпилированную версию экземпляра функции. (Компоновщик знает об этом и разрешает все вызовы одной скомпилированной функции, поэтому в окончательном двоичном файле программы / библиотеки будет только один.) Однако для того, чтобы скомпилировать каждый из исходных файлов, необходимо было создать экземпляр функции и скомпилировано, что потребовало времени.

Компоновщику достаточно для выполнения своей работы, если тело функции находится в одном объектном файле. Создание экземпляра шаблона в исходном файле вручную - это способ заставить компилятор поместить тело функции в объектный файл исходного файла. (Это как если бы функция была вызвана, но создание экземпляра написано в месте, где вызов функции был бы недопустимым.) Когда это будет сделано, все файлы, которые вызывают вашу функцию, могут быть скомпилированы, зная только заголовок функции, таким образом экономия времени, необходимого для создания и компиляции тела функции с каждым из вызовов.

Вторая причина (сокрытие реализации) теперь может иметь смысл. Если автор библиотеки хочет, чтобы пользователи ее функции шаблона могли использовать эту функцию, он обычно дает им код шаблона, чтобы они могли скомпилировать его самостоятельно. Если она хотела сохранить в секрете исходный код шаблона, она могла бы вручную создать экземпляр шаблона в коде, который она использует для построения библиотеки, и предоставить пользователям полученную таким образом версию объекта вместо исходного кода.

Есть ли в этом смысл?

Другие вопросы по тегам