【问题标题】:Why does this template function not behave as expected?为什么这个模板函数的行为不像预期的那样?
【发布时间】:2020-03-24 17:41:24
【问题描述】:

我正在阅读有关模板函数的内容,但被这个问题弄糊涂了:

#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&lt;double&gt;(double);,结果是一样的。

我认为g&lt;double&gt; 应该在f(double) 之后实例化,因此在g 中对f 的调用应该调用f(double)。令人惊讶的是,它仍然在g&lt;double&gt; 中调用f(int)。谁能帮我理解这个?


阅读完答案后,我明白了我的困惑到底是什么。

这是一个更新的示例。除了我为g&lt;double&gt; 添加了一个专业化之外,它几乎没有变化:

#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&lt;double&gt; 执行相同的实例化(或者甚至在main() 之后,如The C++ Programming Language 第 4 版第 26.3.3 节所述) ?

【问题讨论】:

  • 最后一个电话,g(1),给我i f(int)。你写了d f(double)。这是一个错字吗?
  • 是的。对不起。更新
  • 模板的基本原理是支持对用户类型的操作,同时仍然防止用户声明的符号劫持内部库调用。这是一个不可能的妥协,因为模板没有“概念”合同,现在引入这样的合理“合同”为时已晚。

标签: c++ function-templates name-lookup dependent-name unqualified-name


【解决方案1】:

名称f 是一个依赖名称(它通过参数val 依赖于T),它将被解析为two steps

  1. 非 ADL 查找检查函数声明...从 模板定义上下文可见。
  2. ADL 检查从模板定义上下文模板实例化上下文可见的函数声明。

void f(double) 在模板定义上下文中不可见,ADL 也找不到它,because

对于基本类型的参数,相关的命名空间和类集是空的


我们可以稍微修改你的例子:

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))而不是f(val)来禁用ADL。那么输出将是6Double f(Int),与您的示例一致。

【讨论】:

  • 非常感谢。我想知道 g 的实例化在代码中的位置。是否就在 main() 之前。如果是这样,实例化的 g 定义是不是应该能够同时看到 f(int) 和 f(double),最后选择 f(double)?
  • @ZhongqiCheng 在第 1 步中,只会考虑模板 definition 上下文,并且从该上下文中 void f(double) 不可见 - 此上下文在其声明之前结束。在第 2 步,ADL 将找不到任何内容,因此模板 实例化 上下文在此处不起任何作用。
  • @ZhongqiCheng,在您的编辑中,您在void f(double) 之后引入了一个定义,因此可以从中看到此功能。现在f 不是从属名称。如果在g&lt;double&gt; 的定义之后声明的f(val); 有更好的匹配,也将找不到。 “向前看”的唯一方法是 ADL(或一些没有正确实现两阶段查找的旧编译器)。
  • 这是我对您的回答的理解。我应该假设函数模板(g 和 g)在模板定义之后立即实例化。因此它不会看到 f(double)。它是否正确。非常感谢。
  • @ZhongqiCheng,在main() 之前实例化。他们不会看到f(double),因为当实例化发生时,为时已晚:查找的第一阶段已经完成,它没有找到f(double)
【解决方案2】:

问题是f(double) 在你调用它的时候没有被声明;如果你把它的声明移到template g前面,它就会被调用。

编辑:为什么要使用手动实例化?

(我将只讨论函数模板,类模板也有类似的论证。)主要用途是减少编译时间和/或对用户隐藏模板的代码。

C++ 程序分两步构建到二进制文件中:编译和链接。为了成功编译函数调用,只需要函数的标头。为了链接成功,需要一个包含函数编译体的目标文件。

现在,当编译器看到一个 templated 函数的调用时,它会做什么取决于它是否知道模板的主体或仅知道标头。如果它只看到头文件,它会做与没有模板化函数一样的事情:将有关链接器调用的信息放入目标文件。但如果它也看到模板的主体,它还会做另一件事:实例化主体的适当实例,编译该主体并将其放入目标文件中。

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

如果函数的主体在一个目标文件中,链接器就足够了。在源文件中手动实例化模板是一种使编译器将函数体放入相关源文件的目标文件中的方法。 (有点好像函数被调用了,但是实例化写在函数调用无效的地方。)完成后,所有调用函数的文件都可以编译,只知道函数的头,因此节省每次调用时实例化和编译函数体所需的时间。

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

这有意义吗?

【讨论】:

  • 如果您能解释一下作者第一个代码中呈现的实例化与作者编辑后的第二个代码中的特化之间的区别,我将不胜感激。我已经多次阅读 cppreference 关于专业化和实例化的网站和书籍,但我不明白。谢谢
  • @Dev:请详细说明您的问题,我不知道该回答什么。基本上在这种情况下,区别在于当特化存在时编译器使用它,而当它不存在时,编译器获取模板,生成它的实例并使用这个生成的实例。在上面的代码中,模板的特化和实例导致相同的代码。
  • 我的问题恰好与代码的那部分有关:“template void g(double);”如果你知道的话,它在编程模板中被命名为实例化。专业化有点不同,因为它在作者发送的第二个代码中声明“template void g(double val) { cout
  • @Dev 我已经尝试过这样做:编译器尽可能使用专业化;如果它看不到特化(例如因为没有),编译器会创建模板的一个实例并使用该实例。在上面的代码中,模板和特化都导致相同的结果,因此唯一的区别在于编译器为获得该结果所做的工作。在其他情况下,特化可以包含任何实现,它不必与模板有任何共同之处(但对于方法头)。更清晰?
  • template void g&lt;double&gt;(double); 是所谓的手动实例化(注意template 没有尖括号,这是语法的一个显着特征);它告诉编译器创建该方法的一个实例。这里影响不大,如果不存在,编译器会在调用实例的地方生成实例。手动实例化是很少使用的功能,我会在你确认事情变得更清楚之后说为什么你可能想要使用它:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-16
  • 2017-04-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多