为何内置类型参数的全局函数需在模板定义前可见才能被无限定调用?
摘要
给定如下Foo的定义,为何调用
Foo{2}();
仅当draw(int);的声明早于Foo定义时才能成功?
非标准术语式解答
《C++ Templates - The Complete Guide》的§14.3.2中有非常相似的示例,大致原因是:当模板首次被解析时,会查找所有非限定依赖名称(例如以下operator()中的draw,它因t是模板参数T类型而成为依赖名称)。在本例中,此时draw(int)尚未可见。后续解析调用Foo{2}();时,仅会执行ADL(参数依赖查找),而int没有关联的命名空间,因此无法找到draw(int)。
但如何从C++标准层面理解这一点?答案应该在[basic.lookup]章节中,能否有人指引我解读它?
详细版本
考虑以下代码:
- 这些头文件:
- 一个暴露带调用运算符的类模板,会对模板类型的对象调用无限定的
draw函数:// foo.hpp template<typename T> struct Foo { T t; void operator()() { draw(t); } }; - 一个暴露命名空间中的类及其关联的
draw函数:// bar.hpp namespace bar { struct Bar {}; void draw(Bar const&); } - 一个暴露全局作用域下针对
int的draw函数:// drawInt.hpp void draw(int);
- 一个暴露带调用运算符的类模板,会对模板类型的对象调用无限定的
- 对应的cpp文件:
// the cpp files void draw(int) { std::cout << "int" << std::endl; } namespace bar { void draw(Bar const&){ std::cout << "Bar" << std::endl; } } - 以及
main函数:// main.cpp #include "drawInt.hpp" #include "bar.hpp" #include "foo.hpp" int main() { Foo{bar::Bar{}}(); Foo{2}(); }
使用以下命令编译并执行:
g++ -std=c++20 *.cpp -O0 -o main && ./main
编译执行成功,输出:
Bar int
对void bar::draw(bar::Bar)的调用可通过ADL实现,我们甚至无需让bar::Bar和bar::draw在Foo定义前声明,代码即可正常工作——例如将
#include "bar.hpp" #include "foo.hpp"
改为
#include "foo.hpp" #include "bar.hpp"
对程序无影响(生成的二进制文件完全相同)。毕竟,在触发模板类型参数T推导的代码行
Foo{bar::Bar{}}();
出现前,编译器无法有效解析operator()的内部逻辑。
另一方面,对应Foo{2}();的void draw(int)调用无法通过ADL解析。
但为何这意味着该draw重载的声明必须早于**Foo的定义,进而导致main中头文件的包含顺序变得重要?**
毕竟,当解析到Foo{2}();时,void draw(int)已经可见。
无论原因是什么,这是否属于设计缺陷?
另外,如果无法将ADL与针对内置类型的非ADL调用无缝结合,ADL的优势何在?这让我想到了Sean Parent的运行时概念惯用法(ADL调用位于其演讲的35:57处)。
内容的提问来源于Stack Exchange,提问作者Enlico

