如何在编译期实现类型特征:确保仅通过ADL可调用foo(T)且不存在全局作用域的::foo(T)
嘿,我完全懂你的痛点!你搞了个ADL定制点,结果遇到了一个恶心的偶发bug:用户不小心把foo定义在全局作用域时,代码有时候能跑有时候不行,全看include顺序。你想做个类型特征,把这种错误直接扼杀在编译期——确保只有通过ADL找到的foo(T)才算数,全局的::foo(T)直接被排除。咱们来一步步解决这个问题。
先理清楚你的核心需求
你需要的类型特征要同时满足两个条件:
- 无限定调用
foo(T)是合法的(也就是能通过ADL找到对应的重载) - 全局作用域的
::foo(T)不能被找到(或者说,仅通过普通查找找不到foo)
你之前遇到的问题是,直接写::foo(T)会触发硬错误,没法用SFINAE优雅地处理,这确实是个容易踩的坑。
你的现有场景回顾
先把你现有的代码捋一遍,方便大家理解问题根源:
你在foos命名空间里写了个ADL定制点:
// Foo.hpp namespace foos { inline constexpr struct Foo { template <typename T, std::enable_if_t<AdlFooable<std::decay_t<T>>::value, int> = 0> constexpr bool operator()(T const& x) const { return foo(x); } } foo{}; }
对应的类型特征是检查无限定调用foo是否合法:
// Foo.hpp template <typename T, typename = void> struct AdlFooable : std::false_type {}; template <typename T> struct AdlFooable<T, std::void_t<decltype(foo(std::declval<T const&>()))>> : std::true_type {};
问题出在这儿:如果用户错误地把foo定义在全局作用域:
// Bar.hpp namespace bar { struct Bar {}; } bool foo(bar::Bar);
要是先includeBar.hpp再includeFoo.hpp,普通查找能找到全局的foo,代码能跑;但反过来,include顺序调换后,foos::foo里的foo(x)就找不到全局的foo了,直接编译失败。这种偶发bug排查起来真的头疼。
完美解决方案:区分ADL和普通查找的类型特征
这里的关键技巧是利用C++的一个规则:用括号包裹函数名时,不会触发ADL,只会进行普通名字查找(也就是找当前命名空间和全局作用域的名字)。我们可以用这个规则来做两个检查:
- 无限定的
foo(T)能调用(确保ADL能找到) - 括号包裹的
(foo)(T)不能调用(确保普通查找找不到)
直接上代码,我会给你逐行解释:
#include <type_traits> #include <utility> // 辅助命名空间,用来放检查用的重载函数 namespace adl_check_detail { // 检查1:无限定调用foo(T)是否合法 template <typename T> auto check_has_adl_foo(int) -> decltype(foo(std::declval<T const&>()), std::true_type{}); // 匹配失败时的 fallback,返回false_type template <typename T> auto check_has_adl_foo(long) -> std::false_type; // 检查2:括号包裹的foo调用是否不可行(即普通查找找不到foo) template <typename T> auto check_no_plain_foo(int) -> decltype((foo)(std::declval<T const&>()), std::false_type{}); // 如果括号调用失败,说明普通查找找不到,返回true_type template <typename T> auto check_no_plain_foo(long) -> std::true_type; } // 最终的类型特征:两个检查都通过才返回true template <typename T> struct AdlFooable : std::conjunction< decltype(adl_check_detail::check_has_adl_foo<T>(0)), decltype(adl_check_detail::check_no_plain_foo<T>(0)) > {}; // 方便使用的变量模板 template <typename T> constexpr bool AdlFooable_v = AdlFooable<T>::value;
代码解释:
check_has_adl_foo:用int参数的重载会优先匹配,如果无限定的foo(T)能合法调用,就返回std::true_type;否则匹配long参数的重载,返回std::false_type。check_no_plain_foo:同样优先匹配int参数的重载,如果(foo)(T)能调用(说明普通查找找到了全局或当前命名空间的foo),就返回std::false_type;如果这个调用失败(普通查找找不到),就匹配long参数的重载,返回std::true_type。AdlFooable:用std::conjunction把两个检查的结果结合起来,只有两个都为true时,整个类型特征才是true_type——也就是只有ADL能找到foo的情况才会通过检查。
把这个类型特征替换你原来的AdlFooable之后,再遇到用户把foo定义在全局作用域的情况,编译器会直接报错,而不是偶发地工作或失败,完美解决了你的问题!
关于你提到的非解决方案
你之前试过用(foo)(args)来抑制ADL,但没成功——其实是你用反了方向!你当时可能是想检查(foo)(args)是否合法,但我们这里是反过来:只有当(foo)(args)不合法时,才是我们想要的情况,所以用重载的方式来捕获这个失败的场景,就能优雅地用SFINAE处理了。
备注:内容来源于stack exchange,提问作者Enlico

