为什么这个模板函数的行为不如预期?

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)之后实例化,因此在g中对f的调用应该调用f(double)。令人惊讶的是,在g<double>中仍然调用f(int)。有人能帮我理解这个问题吗?
阅读答案后,我明白了我的困惑所在。
这是一个更新的例子,除了我添加了一个针对 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)的行为与我预期的一样。

编译器是否不会在同一位置(或甚至在main()之后,如第4版The C++ Programming Language的26.3.3节中所述)自动进行g<double>的实例化?


3
最后一次调用 g(1) 对我来说返回的是 i f(int)。而你写的是 d f(double),这是打字错误吗?请确认。 - HTNW
是的。抱歉。已更新。 - Zhongqi Cheng
模板的基本原则是支持对用户类型进行操作,同时防止用户声明的符号劫持内部库调用。这是一种不可能的妥协,因为模板没有“概念”合同,并且现在引入这样的“合同”已经太晚了。 - curiousguy
2个回答

12
名称f是一个依赖名称(它通过参数val依赖于T),将会分为两个步骤解析:
  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))而不是f(val)来禁用ADL。然后输出结果将是6Double f(Int),与您的示例相符。

非常感谢。我想知道代码中g<double>的实例化在哪里。它是不是就在main()之前。如果是这样,那么实例化的g<double>定义应该能够看到f(int)和f(double),最终选择f(double)吧? - Zhongqi Cheng
在第1步中,只会考虑模板的“定义”上下文,在该上下文中void f(double)是不可见的 - 在其声明之前,该上下文已经结束。在第2步中,ADL找不到任何东西,因此模板的“实例化”上下文在这里没有任何作用。 - Evg
@ZhongqiCheng,您在编辑时在void f(double)之后引入了一个定义,因此该函数从中可见。现在f不是依赖名称。如果在g<double>的定义之后声明了更好的匹配项f(val);,它也无法找到。 "向前看"的唯一方法是ADL(或一些未正确实现两阶段查找的旧编译器)。 - Evg
这是我对你答案的理解。我应该假设函数模板(g<int>和g<double>)在模板定义之后立即实例化。因此它不会看到f(double)。 这正确吗?非常感谢。 - Zhongqi Cheng
main()之前实例化的@ZhongqiCheng。他们看不到f(double),因为当实例化发生时,为查找的第一阶段已经完成,并且没有找到f(double) - Evg

6
问题在于你调用它的时候,编译器还没有声明;如果你在

@开发者:请更明确地说明你的问题,我不确定该回答什么。基本上,在这种情况下的不同之处在于当存在特化时,编译器使用它,而当不存在特化时,编译器会采取模板,生成一个实例并使用此生成的实例。在上面的代码中,特化和模板实例都将导致相同的代码。 - AshleyWilkes
我的问题确切地涉及到代码的这一部分:“template void g<double>(double);”在编程模板中,它被称为实例化,如果你知道的话。特化有点不同,因为它像作者发送的第二段代码那样声明“template<>void g<double>(double val){cout << typeid(val).name() << " ";f(val);}”。你能解释一下它们之间的区别吗? - Dev
是的,在他添加专业化之前,只有“template void g<double>(double);”,我不理解它是如何工作的。 - Dev
1
template void g<double>(double);被称为手动实例化(请注意,这里的template没有尖括号,这是语法的一个区别特征);它告诉编译器创建该方法的一个实例。在这里,它几乎没有影响,如果没有它,编译器将在调用实例的地方生成实例。手动实例化是一种很少使用的功能,我会在确认事情变得更清晰后解释为什么您可能想要使用它:-) - AshleyWilkes
事情开始变得更加清晰。我仍然需要理解如何使用它,在哪种情况下,因为很少有简单的解释。 - Dev
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接