另一种阻止 ADL 的巧妙手法
在 TBOMC++ (The Book of Modern C++) 第一章中,介绍过利用 ()
阻止 ADL 的技巧,本篇介绍另一种更加巧妙且有用的阻止 ADL 的方法。
介绍这种方法之前,先讲解一下它背后的规则和原理。
ADL 的参数类型为一个类时,不仅会查找该类本身,还会查找与其关联的实体。但是,关联实体并不会递归式地一直查找下去,看下面的例子:
struct A {
struct B {
struct C {
};
};
friend void f(...) { std::println("f"); }
};
f(A{}); // OK
f(A::B{}); // OK
f(A::B::C{}); // Err
此时,A
的关联实体是 A::B
,A::B
的关联实体是 A::B::C
,而 A::B::C
并不是 A
的关联实体。换言之,这种查找方式并不具备可传递性,只在类及其成员之间有效(涉及父类的情况请参考 TBOMC++)。因此,f()
在参数类型为 A
或 A::B
时能够被 ADL 查找到,而在 A::B::C
时查找失败。
至于模板,规则也大抵如此,例如:
struct D {
friend void g(...) { std::println("g"); }
};
template<class>
struct E {
struct F {};
};
g(D{}); // OK
g(E<D>{}); // OK
g(E<D>::F{}); // Err
此时,D
的关联实体是 E<D>
,E<D>
的关联实体是 E<D>::F
,而 E<D>::F
并不是 D
的关联实体。
于是,可以得出一个结论:非关联实体可以阻止 ADL。这就是技巧背后的核心思路。
那什么时候需要阻止 ADL 呢?考虑下面的场景:
template<class> struct type_tag {};
namespace unexpected {
struct S {};
} // namespace unexpected
namespace N = unexpected;
namespace mylib {
void bar(const type_tag<N::S>&) {
std::println("mylib::bar()");
}
template<class T>
void test() {
bar(type_tag<T>{});
}
} // namespace mylib
bar()
是某个功能函数,test()
测试这个功能函数,可以这样调用 test()
:
// Output: mylib::bar()
mylib::test<unexpected::S>();
行为一切正常。若是 unexpected
不知道 mylib
里面包含了一个 bar()
函数,完全可能写出以下代码:
namespace unexpected {
struct S {};
template<class T>
void bar(T&&) {
std::println("unexpected::bar()");
}
} // namespace unexpected
// Output: unexpected::bar()
mylib::test<unexpected::S>();
此时,unexpected
中定义的 bar()
函数劫持了 mylib
内部的 bar()
函数!unexpected::S
是 type_tag<unexpected::S>
的关联实体。因此,unexpected
里面的 bar()
也会被考虑,重载集中包含 2 个函数:
// overload function 1
void bar(const type_tag<N::S>&);
// overload function 2
template<class T> void userspace::bar(T&&);
第 1 个重载函数是通过 Usual Unqualified Lookup 查找到的,而第 2 个重载函数是通过 ADL 查找到的。第 2 个是函数模板,所以接着进行模板处理。实参类型为 type_tag<unexpected::S>
,模板参数替换后为:
// candidate function 2
namespace unexpected {
template<>
void bar<type_tag<S>>(type_tag<S>&&);
} // namespace unexpected
一级筛选结束,这两个重载函数全部进入了候选函数列表;接着进行二级筛选,这两个函数也全部通过,进入可行函数列表;最后进入决胜局,模板函数更加匹配,遂胜出。
倘若不希望发生这种情况,只有通过阻止 ADL 来实现。
第一种方式就是 TBOMC++ 中介绍的 ()
技巧:
namespace mylib {
template<class T>
void test() {
// Use parentheses to prevent ADL
(bar)(type_tag<T>{});
}
} // namespace mylib
这完全能够达到目的,但是只能在库的内部用,外部如果也有这样的需求,这种解决方式就太奇怪了。
第二种方式就是以非关联实体阻止 ADL。其他代码无须改变,只需要把 type_tag
的实现变为:
template<class>
struct type_tag_impl {
struct type {};
};
template<class NonAssociated>
using type_tag = type_tag_impl<NonAssociated>::type;
这样就不可能再调用 unexpected
内部的重载函数,因为 ADL 也被阻止了。
此时,bar()
的参数类型为 type_tag_impl<unexpected::S>::type
,它的关联实体是 type_tag_impl<unexpected::S>
。因为这种 ADL 查找不具备可传递性,所以不会查找到 unexpected::S
,它所关联的 unexpected
命名空间自然也就不会被查找到了。
结果就是,重载集中不会再包含 ADL 查找到的函数,任何非预期的函数劫持都被杜绝了。
这个技巧源于(?)Boost.Hana 库,大家也可以结合这篇文章一起阅读,我参考了其中的例子。
标准中也用到了该手法,本篇只介绍原理和结构,后续再来展开实际中的应用。