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

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
Основной принцип шаблона заключается в поддержке использования операций над пользовательскими типами, в то же время предотвращая захват внутренних библиотечных вызовов объявленными пользователем символами. Что является невозможным компромиссом, поскольку для шаблонов нет «концептуальных» контрактов, и уже слишком поздно вводить такие надежные «контракты».
любопытный парень

Ответы:

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

Evg
источник
Большое спасибо. Мне интересно, где находится экземпляр 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 (double). Это правильно. Огромное спасибо.
Zhongqi Cheng
@ZhongqiCheng, созданный прямо перед тем main(). Они не увидят f(double), потому что, когда происходит создание экземпляра, уже слишком поздно: первый этап поиска уже завершен, и он не нашел f(double).
Evg
6

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

Редактировать: Почему можно использовать ручную реализацию?

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

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

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

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

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

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

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

AshleyWilkes
источник
Буду признателен, если вы сможете объяснить разницу между экземпляром, представленным в первом коде автора, и специализацией во втором коде автора после редактирования. Я много раз читал сайт cppreference о специализации и создании и книгах, но я не понял. Спасибо
Дев
@Dev: Пожалуйста, укажите ваш вопрос немного больше, я не уверен, что ответить. По сути, в этом случае разница состоит в том, что когда специализация присутствует, компилятор использует ее, а когда ее нет, компилятор берет шаблон, генерирует его экземпляр и использует этот сгенерированный экземпляр. В приведенном выше коде специализация и экземпляр шаблона приводят к одному и тому же коду.
AshleyWilkes
Мой вопрос точно касается этой части кода: "template void g <double> (double);" Это называется экземпляром в шаблоне программирования, если вы это знаете. Специализация немного отличается, так как она объявляется так же, как и во втором коде, автор послал "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Не могли бы вы объяснить мне разницу?
Dev
@Dev Я уже пытался это сделать: компилятор использует специализацию, если это возможно; если он не видит специализацию (например, потому что ее нет), компилятор создает экземпляр шаблона и использует этот экземпляр. В приведенном выше коде и шаблон, и специализация приводят к одному и тому же результату, поэтому единственное различие заключается в том, что компилятор делает для достижения этого результата. В других случаях специализация может содержать любую реализацию, она не должна иметь ничего общего с шаблоном (кроме заголовка метода). Яснее?
AshleyWilkes
1
Это template void g<double>(double);так называемое ручное создание экземпляра (обратите внимание, что templateбез угловых скобок это отличительная черта синтаксиса); это говорит компилятору создать экземпляр метода. Здесь это имеет небольшой эффект, если бы его не было, компилятор генерировал бы экземпляр в том месте, где он вызывается. Ручная инстанция - редко используемая функция, я скажу, почему вы можете захотеть использовать ее после того, как подтвердите, что теперь все
понятно