ADL的缺陷是什么?

时间:2010-06-02 14:28:20

标签: c++ namespaces overload-resolution argument-dependent-lookup

前段时间我读了一篇文章解释了参数依赖查找的几个缺陷,但我再也找不到了。它是关于获取您不应该访问的东西或类似的东西。所以我想我会在这里问:ADL的缺陷是什么?

2 个答案:

答案 0 :(得分:64)

依赖于参数的查找存在一个巨大的问题。例如,考虑以下实用程序:

#include <iostream>

namespace utility
{
    template <typename T>
    void print(T x)
    {
        std::cout << x << std::endl;
    }

    template <typename T>
    void print_n(T x, unsigned n)
    {
        for (unsigned i = 0; i < n; ++i)
            print(x);
    }
}

这很简单,对吗?我们可以调用print_n()并将其传递给任何对象,它会调用print来打印对象n次。

实际上,事实证明,如果我们只查看此代码,我们完全不知道 print_n将调用哪个函数。它可能是此处给出的print函数模板,但可能不是。为什么?依赖于参数的查找。

举个例子,假设你写了一个代表独角兽的类。出于某种原因,你还定义了一个名为print的函数(真是巧合!)只是通过写入一个解除引用的空指针(谁知道你为什么这样做;这并不重要)导致程序崩溃:

namespace my_stuff
{
    struct unicorn { /* unicorn stuff goes here */ };

    std::ostream& operator<<(std::ostream& os, unicorn x) { return os; }

    // Don't ever call this!  It just crashes!  I don't know why I wrote it!
    void print(unicorn) { *(int*)0 = 42; }
}

接下来,你编写一个小程序,创建一个独角兽并打印四次:

int main()
{
    my_stuff::unicorn x;
    utility::print_n(x, 4);
}

你编译这个程序,运行它,然后......它崩溃了。 “什么?!没办法,”你说:“我刚刚调用了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 ++标准委员会都没有接受它们。

答案 1 :(得分:2)

公认的答案完全是错误的-这不是ADL的错误。它显示了一个粗心的反模式,该模式在日常编码中使用函数调用-对依赖名称的无知,并盲目地依赖不合格的函数名称。

简而言之,如果您在函数调用的postfix-expression中使用了不合格的名称,则您应该承认您已授予可以在其他位置“覆盖”该功能的能力。 (是的,这是一种静态多态性)。因此,C ++中函数的不合格名称的拼写恰好是接口的一部分。

在接受答案的情况下,如果print_n确实需要ADL print(即允许它被覆盖),则应该使用不合格的print对其进行记录作为明确的通知,客户将收到一份合同,约定print应当经过仔细声明,并且不当行为将是my_stuff的全部责任。否则,它是print_n的错误。解决方法很简单:用前缀print限定utility::。这确实是print_n的错误,但几乎不是该语言中ADL规则的错误。

但是,在语言规范中,存在要做不需要的东西,从技术上讲,不是只有一个。他们已经实现了10多年,但是该语言的任何内容都尚未确定。他们被所接受的答案错过了(除非最后一段到目前为止是唯一正确的)。有关详情,请参见此paper

我可以针对姓名查找讨厌添加一个实际案例。我正在实现is_nothrow_swappable,其中__cplusplus < 201703L。我发现一旦我在命名空间中有一个声明的swap函数模板,就不可能依靠ADL来实现这种功能。这样的swap总是会和惯用std::swap引入的using std::swap;一起使用,以根据ADL规则使用ADL,然后swap就会变得模棱两可,其中{{1 }}模板(将实例化swap以获得正确的is_nothrow_swappable)被调用。一旦包含了noexcept-specification模板的库头,便与两阶段查找规则结合使用,声明的顺序不计算在内。因此,除非我使用专门的swap函数将所有库类型的 all 重载(以抑制ADL之后通过重载分辨率匹配的任何候选通用模板swap),否则无法声明该模板。具有讽刺意味的是,在我的命名空间中声明的swap模板正是利用ADL(考虑swap),并且它是我的库中boost::swap的最重要的直接客户端之一(BTW,{{ 1}}不遵守异常规范。这完全打败了我的目的,感叹……

is_nothrow_swappable

尝试https://wandbox.org/permlink/4pcqdx0yYnhhrASi,然后将boost::swap转到#include <type_traits> #include <utility> #include <memory> #include <iterator> namespace my { #define USE_MY_SWAP_TEMPLATE true #define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false namespace details { using ::std::swap; template<typename T> struct is_nothrow_swappable : std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))> {}; } // namespace details using details::is_nothrow_swappable; #if USE_MY_SWAP_TEMPLATE template<typename T> void swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::value) { // XXX: Nasty but clever hack? std::iter_swap(std::addressof(x), std::addressof(y)); } #endif class C {}; // Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library? #if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE void swap(C&, C&) noexcept {} #endif } // namespace my int main() { my::C a, b; #if USE_MY_SWAP_TEMPLATE my::swap(a, b); // Even no ADL here... #else using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone. swap(a, b); // ADL rocks? #endif } ,以查看歧义。

更新2018-11-05:

啊哈,今天早上我又被ADL咬伤了。这次甚至与函数调用无关!

今天,我正在完成将ISO C++17 std::polymorphic_allocator移植到我的代码库的工作。由于一些容器类模板是很早以前在我的代码中引入的(例如this),所以这次我只用别名模板替换声明,例如:

USE_MY_SWAP_TEMPLATE

...,因此默认情况下可以使用my implementation of polymorphic_allocator。 (免责声明:它具有一些已知的错误。这些错误的修复将在几天内提交。)

但是突然出现了数百行的隐式错误消息,它却无法正常工作...

错误从this line开始。它大致抱怨声明的true不是封闭类namespace pmr = ystdex::pmr; template<typename _tKey, typename _tMapped, typename _fComp = ystdex::less<_tKey>, class _tAlloc = pmr::polymorphic_allocator<std::pair<const _tKey, _tMapped>>> using multimap = std::multimap<_tKey, _tMapped, _fComp, _tAlloc>; 的基础。这似乎很奇怪,因为使用与类定义的 base-specifier-list 中相同的标记声明了别名,并且我敢肯定它们中的任何一个都不能宏扩展。那为什么呢?

答案是... ADL很烂。 The line inroducing BaseTypeBaseType名称作为模板参数进行了硬编码,因此将根据ADL规则在类范围内查找该模板。因此,它找到MessageQueue,这与查找结果不同,因为在封闭的名称空间范围中,实际的基类声明为。由于std使用std::multimap实例作为默认模板参数,因此std::multimap与具有实例std::allocator甚至{{1}的实例的实际基类的类型不同封闭的名称空间中声明的}重定向到BaseType。通过将封闭条件作为polymorphic_allocator的前缀权限添加,可以修复该错误。

我承认我很幸运。错误消息将问题引导至此行。只有两个类似的问题,the other没有任何明确的multimap(其中std::multimapmy own one适用于ISO C ++ 17的=更改,不是std(在C ++ 17之前的模式中)。我不会这么快就发现关于ADL的错误。