为什么此模板功能无法正常运行?


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),因此fin 的调用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(),如C ++编程语言,第4版的26.3.3节中所述)是否自动在同一位置进行相同的实例化?


3
最后一个电话g(1)i f(int)我。你写了d f(double)。这是错字吗?
HTNW

是。抱歉。更新
Zhongqi Cheng,

模板的基本原理是支持对用户类型的操作的使用,同时仍防止用户声明的符号劫持内部库调用。这是不可能的折衷方案,因为没有模板的“概念”合同,现在引入这种合理的“合同”为时已晚。
curiousguy19年

Answers:


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)。我们可以通过写(f)(val)(或::f(val))而不是禁用ADL f(val)。然后输出将6Double f(Int)与您的示例一致。


非常感谢你。我想知道g <double>的实例化在代码中。是在main()之前吗?如果是这样,实例化的g <double>定义不应该同时看到f(int)和f(double),最后选择f(double)吗?
Zhongqi Cheng

@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 ++程序通过以下两个步骤构建到二进制文件中:编译和链接。为了成功进行函数调用的编译,仅需要函数的标头。为了使链接成功,需要一个包含函数的已编译主体的目标文件。

现在,当编译器看到模板函数的调用时,其作用取决于它是否知道模板的主体或仅是标题。如果仅看到标头,则其作用与未对函数进行模板化的作用相同:将有关链接程序调用的信息放入目标文件。但是,如果它也看到模板的主体,它还会做另一件事:它实例化主体的正确实例,编译该主体并将其放入目标文件中。

如果多个源文件调用模板化函数的相同实例,则它们的每个目标文件都将包含该函数实例的编译版本。(链接器知道这一点,并将所有调用解析为一个已编译函数,因此在程序/库的最终二进制文件中将只有一个。)但是,为了编译每个源文件,必须实例化该函数并编译,这需要时间。

如果函数的主体位于一个目标文件中,则链接器就可以完成它的工作。手动实例化源文件中的模板是使编译器将函数的主体放入相关源文件的目标文件中的一种方法。(这有点像调用了函数,但是实例化是在函数调用无效的地方编写的。)完成此操作后,可以仅在知道函数头的情况下编译所有调用函数的文件。节省了每次调用实例化和编译函数主体所需的时间。

第二个原因(隐藏实现)现在可能有意义。如果图书馆作者希望其模板功能的用户能够使用该功能,则通常会向他们提供模板的代码,以便他们自己进行编译。如果她想对模板的源代码保密,则可以在用于构建库的代码中手动实例化模板,并向用户提供由此获得的对象版本而不是源代码。

这有意义吗?


如果您能解释作者第一个代码中呈现的实例化与编辑后作者的第二个代码中的特殊化之间的区别,我将不胜感激。我已经多次阅读过cppreference网站上有关专门化和实例化的书籍,但我听不懂。谢谢
开发人员

@Dev:请再详细说明您的问题,我不确定该怎么回答。基本上,在这种情况下,不同之处在于,当存在专用化时,编译器将使用它,而当不存在专用化时,编译器将使用模板,生成其实例,并使用此生成的实例。在上面的代码中,专业化和模板实例均导致同一代码。
AshleyWilkes '19

我的问题正好在代码的那一部分附近:“ template void g <double>(double);” 如果您知道的话,它在编程模板中称为实例化。专业化有点不同,因为它的声明类似于作者在第二个代码中发送的“ template <> void g <double>(double val){cout << typeid(val).name()<<“”; f( val);}“您能告诉我区别吗?
开发人员

@Dev我已经尝试这样做了:如果可以的话,编译器使用特殊化;如果看不到专业化(例如,因为没有专业化),则编译器将创建模板的实例并使用该实例。在上面的代码中,模板和专业化均导致相同的结果,因此唯一的区别在于编译器为获得该结果所做的工作。在其他情况下,专业化可以包含任何实现,它不必与模板有任何共同之处(但对于方法标头)。更清晰?
AshleyWilkes '19

1
template void g<double>(double);是所谓的手动实例化(注意template,没有尖括号,这是该语法的特点); 告诉编译器创建方法的实例。在这里它没有什么效果,如果不在那儿,编译器将在调用实例的位置生成实例。手动实例化是很少使用的功能,我会说为什么在确认现在事情变得更清楚之后为什么要使用它:-)
AshleyWilkes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.