ADL 有哪些陷阱?
前段时间我读过一篇文章,解释了参数依赖查找的几个陷阱,但我再也找不到了。这是关于获取你不应该访问的东西或类似的东西。所以我想我应该在这里问:ADL 有哪些陷阱?
Some time ago I read an article that explained several pitfalls of argument dependent lookup, but I cannot find it anymore. It was about gaining access to things that you should not have access to or something like that. So I thought I'd ask here: what are the pitfalls of ADL?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
依赖于参数的查找存在一个巨大的问题。例如,考虑以下实用程序:
它很简单,对吧?我们可以调用
print_n()
并向其传递任何对象,它会调用print
打印该对象n
次。事实上,事实证明,如果我们只看这段代码,我们完全不知道 print_n 会调用什么函数。它可能是此处给出的
print
函数模板,但也可能不是。为什么?依赖于参数的查找。举个例子,假设您编写了一个类来代表独角兽。出于某种原因,您还定义了一个名为
print
的函数(多么巧合!),它只会通过写入取消引用的空指针而导致程序崩溃(谁知道您为什么这样做;那不是重要):接下来,您编写一个小程序,创建一个独角兽并将其打印四次:
您编译该程序,运行它,然后......它崩溃了。 “什么?!不可能,”你说:“我刚刚调用了
print_n
,它调用了print
函数来打印独角兽四次!”是的,确实如此,但它没有调用您期望它调用的print
函数。它称为my_stuff::print
。为什么选择
my_stuff::print
?在名称查找过程中,编译器发现调用print
的参数属于unicorn
类型,它是在命名空间my_stuff< 中声明的类类型。 /代码>。
由于依赖于参数的查找,编译器在搜索名为
print
的候选函数时包含此命名空间。它找到my_stuff::print
,然后在重载解析期间将其选为最佳可行候选:调用任一候选print
函数都不需要转换,并且非模板函数是首选函数模板,因此非模板函数my_stuff::print
是最佳匹配。(如果您不相信这一点,您可以按原样编译此问题中的代码并查看 ADL 的实际操作。)
是的,依赖于参数的查找是 C++ 的一个重要功能。本质上需要实现某些语言功能的所需行为,例如重载运算符(考虑流库)。也就是说,它也有非常非常有缺陷,并且可能导致非常严重的问题。已经有几个修复参数相关查找的提案,但没有一个被 C++ 标准委员会接受。
There is a huge problem with argument-dependent lookup. Consider, for example, the following utility:
It's simple enough, right? We can call
print_n()
and pass it any object and it will callprint
to print the objectn
times.Actually, it turns out that if we only look at this code, we have absolutely no idea what function will be called by
print_n
. It might be theprint
function template given here, but it might not be. Why? Argument-dependent lookup.As an example, let's say you have written a class to represent a unicorn. For some reason, you've also defined a function named
print
(what a coincidence!) that just causes the program to crash by writing to a dereferenced null pointer (who knows why you did this; that's not important):Next, you write a little program that creates a unicorn and prints it four times:
You compile this program, run it, and... it crashes. "What?! No way," you say: "I just called
print_n
, which calls theprint
function to print the unicorn four times!" Yes, that's true, but it hasn't called theprint
function you expected it to call. It's calledmy_stuff::print
.Why is
my_stuff::print
selected? During name lookup, the compiler sees that the argument to the call toprint
is of typeunicorn
, which is a class type that is declared in the namespacemy_stuff
.Because of argument-dependent lookup, the compiler includes this namespace in its search for candidate functions named
print
. It findsmy_stuff::print
, which is then selected as the best viable candidate during overload resolution: no conversion is required to call either of the candidateprint
functions and nontemplate functions are preferred to function templates, so the nontemplate functionmy_stuff::print
is the best match.(If you don't believe this, you can compile the code in this question as-is and see ADL in action.)
Yes, argument-dependent lookup is an important feature of C++. It is essentially required to achieve the desired behavior of some language features like overloaded operators (consider the streams library). That said, it's also very, very flawed and can lead to really ugly problems. There have been several proposals to fix argument-dependent lookup, but none of them have been accepted by the C++ standards committee.
接受的答案是完全错误的 - 这不是 ADL 的错误。它显示了在日常编码中使用函数调用的粗心反模式 - 忽视依赖名称并盲目依赖不合格的函数名称。
简而言之,如果您在函数调用的后缀表达式中使用非限定名称,您应该承认您已授予该函数可以“重写”的能力" 其他地方(是的,这是一种静态多态性)。因此,C++ 中函数的非限定名称的拼写正是接口的一部分。
在接受答案的情况下,如果
print_n
确实需要 ADLprint
(即允许它被覆盖),则应该使用不合格的对其进行记录>print
作为明确的通知,因此客户将收到一份合同,要求print
应仔细声明,并且不当行为将由my_stuff
承担全部责任。否则就是print_n
的bug。修复方法很简单:使用前缀utility::
限定print
。这确实是print_n
的一个错误,但几乎不是该语言中 ADL 规则的错误。然而,语言规范中确实存在不需要的东西,而且从技术上讲,不只是一个。它们的实现已有 10 多年的时间,但语言中的任何内容尚未得到解决。他们被接受的答案所错过(除了最后一段到目前为止是唯一正确的)。有关详细信息,请参阅此论文。
我可以附加一个针对令人讨厌的名称查找的真实案例。我正在实现 is_nothrot_swappable ,其中 __cplusplus
201703L。我发现一旦我的命名空间中声明了
swap
函数模板,就不可能依赖 ADL 来实现此类功能。这种swap
总是与惯用的using std::swap;
引入的std::swap
一起找到,以便在 ADL 规则下使用 ADL,然后就会出现swap
的歧义,其中swap
模板(它将实例化is_nothrow_swappable
以获得正确的noexcept-specation
代码>) 被调用。结合两阶段查找规则,一旦包含包含swap
模板的库标头,声明的顺序就不再计算在内。因此,除非我使用专门的swap
函数重载所有我的库类型(以抑制 ADL 之后的重载解析匹配任何候选通用模板swap
) ,我无法声明模板。讽刺的是,在我的命名空间中声明的swap
模板正是利用 ADL(考虑boost::swap
),并且它是is_nothrow_swappable 最重要的直接客户端之一
在我的库中(顺便说一句,boost::swap
不遵守异常规范)。这完美地达到了我的目的,叹息......尝试 https://wandbox.org/permlink/4pcqdx0yYnhhrASi 并将
USE_MY_SWAP_TEMPLATE
设置为true
以查看歧义。更新2018-11-05:
啊哈,今天早上我又被ADL咬伤了。这次它甚至与函数调用无关!
今天完成移植 ISO C++17
std::polymorphic_allocator 的工作
到我的代码库。由于我的代码中很久以前就引入了一些容器类模板(例如 this),这次我只是用别名模板替换声明,例如:...所以它可以使用 我默认实现的
polymorphic_allocator
。 (免责声明:它有一些已知的错误。错误的修复将在几天内提交。)但是它突然不起作用,有数百行神秘的错误消息......
错误从
答案是……ADL 很糟糕。 引入
BaseType
的行是使用std
名称作为模板参数进行硬编码的,因此将查找模板根据类范围内的 ADL 规则。因此,它找到了 std::multimap,这与在封闭命名空间范围中声明的实际基类的查找结果不同。由于std::multimap
使用std::allocator
实例作为默认模板参数,因此BaseType
与实际基类的类型不同,有一个polymorphic_allocator
实例,即使在封闭命名空间中声明的multimap
也会重定向到std::multimap
。通过添加封闭限定作为=
的前缀,该错误得到修复。我承认我足够幸运。错误消息将问题引向这一行。只有2个类似的问题和 另一个 没有任何明确的
std
(其中string
是 我自己的正在适应ISO C++17的string_view
更改,而不是C++17之前模式中的std
)。我不会这么快就发现这个 bug 是关于 ADL 的。The accepted answer is simply wrong - this is not a bug of ADL. It shows an careless anti-pattern to use function calls in daily coding - ignorance of dependent names and relying on unqualified function names blindly.
In short, if you are using unqualified name in the
postfix-expression
of a function call, you should have acknowledged that you have granted the ability that the function can be "overridden" elsewhere (yes, this is a kind of static polymorphism). Thus, the spelling of the unqualified name of a function in C++ is exactly a part of the interface.In the case of the accepted answer, if the
print_n
really need ADLprint
(i.e. allowing it to be overridden), it should have been documented with the use of unqualifiedprint
as an explicit notice, thus clients would receive a contract thatprint
should be carefully declared and the misbehavior would be all of the responsibility ofmy_stuff
. Otherwise, it is a bug ofprint_n
. The fix is simple: qualifyprint
with prefixutility::
. This is indeed a bug ofprint_n
, but hardly a bug of the ADL rules in the language.However, there do exist unwanted things in the language specification, and technically, not only one. They are realized more than 10 years, but nothing in the language is fixed yet. They are missed by the accepted answer (except that the last paragraph is solely correct till now). See this paper for details.
I can append one real case against the name lookup nasty. I was implementing
is_nothrow_swappable
where__cplusplus < 201703L
. I found it impossible to rely on ADL to implementing such feature once I have a declaredswap
function template in my namespace. Suchswap
would always found together withstd::swap
introduced by a idiomaticusing std::swap;
to use ADL under the ADL rules, and then there would come ambiguity ofswap
where theswap
template (which would instantiateis_nothrow_swappable
to get the propernoexcept-specification
) is called. Combined with 2-phase lookup rules, the order of declarations does not count, once the library header containing theswap
template is included. So, unless I overload all my library types with specializedswap
function (to supress any candidate generic templatesswap
being matched by overloading resolution after ADL), I cannot declare the template. Ironically, theswap
template declared in my namespace is exactly to utilize ADL (considerboost::swap
) and it is one of the most significant direct client ofis_nothrow_swappable
in my library (BTW,boost::swap
does not respects the exception specification). This perfectly beat my purpose up, sigh...Try https://wandbox.org/permlink/4pcqdx0yYnhhrASi and turn
USE_MY_SWAP_TEMPLATE
totrue
to see the ambiguity.Update 2018-11-05:
Aha, I am bitten by ADL this morning again. This time it even has nothing to do with function calls!
Today I am finishing the work of porting ISO C++17
std::polymorphic_allocator
to my codebase. Since some container class templates have been introduced long ago in my code (like this), this time I just replace the declarations with alias templates like:... so it can use my implementation of
polymorphic_allocator
by default. (Disclaimer: it has some known bugs. Fixes of the bugs would be committed in a few days.)But it suddenly does not work, with hundreds of lines of cryptic error messages...
The error begins from this line. It roughly complains that the declared
BaseType
is not a base of the enclosing classMessageQueue
. That seems very strange because the alias is declared with exactly the same tokens to those in the base-specifier-list of the class definition, and I am sure nothing of them can be macro-expanded. So why?The answer is... ADL sucks. The line inroducing
BaseType
is hard-coded with astd
name as a template argument, so the template would be looked up per ADL rules in the class scope. Thus, it findsstd::multimap
, which differs to the result of lookup in as the actual base class declared in the enclosing namespace scope. Sincestd::multimap
usesstd::allocator
instance as the default template argument,BaseType
is not the same type to the actual base class which have an instance ofpolymorphic_allocator
, evenmultimap
declared in the enclosing namespace is redirected tostd::multimap
. By adding the enclosing qualification as the prefix right to the=
, the bug is fixed.I'd admit I am lucky enough. The error messages are heading the problem to this line. There are only 2 similar problems and the other is without any explicit
std
(wherestring
is my own one being adapted to ISO C++17'sstring_view
change, notstd
one in pre-C++17 modes). I would not figure out the bug is about ADL so quickly.