在 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::BA::B 的关联实体是 A::B::C,而 A::B::C 并不是 A 的关联实体。换言之,这种查找方式并不具备可传递性,只在类及其成员之间有效(涉及父类的情况请参考 TBOMC++)。因此,f() 在参数类型为 AA::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::Stype_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 库,大家也可以结合这篇文章一起阅读,我参考了其中的例子。

标准中也用到了该手法,本篇只介绍原理和结构,后续再来展开实际中的应用。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.