ADL 有哪些陷阱?

发布于 2024-09-04 02:52:23 字数 87 浏览 6 评论 0原文

前段时间我读过一篇文章,解释了参数依赖查找的几个陷阱,但我再也找不到了。这是关于获取你不应该访问的东西或类似的东西。所以我想我应该在这里问: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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

勿忘心安 2024-09-11 02:52:23

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

#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++ 标准委员会接受。

There is a huge problem with argument-dependent lookup. Consider, for example, the following utility:

#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);
    }
}

It's simple enough, right? We can call print_n() and pass it any object and it will call print to print the object n 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 the print 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):

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; }
}

Next, you write a little program that creates a unicorn and prints it four times:

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

You compile this program, run it, and... it crashes. "What?! No way," you say: "I just called print_n, which calls the print function to print the unicorn four times!" Yes, that's true, but it hasn't called the print function you expected it to call. It's called my_stuff::print.

Why is my_stuff::print selected? During name lookup, the compiler sees that the argument to the call to print is of type unicorn, which is a class type that is declared in the namespace my_stuff.

Because of argument-dependent lookup, the compiler includes this namespace in its search for candidate functions named print. It finds my_stuff::print, which is then selected as the best viable candidate during overload resolution: no conversion is required to call either of the candidate print functions and nontemplate functions are preferred to function templates, so the nontemplate function my_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.

[旋木] 2024-09-11 02:52:23

接受的答案是完全错误的 - 这不是 ADL 的错误。它显示了在日常编码中使用函数调用的粗心反模式 - 忽视依赖名称并盲目依赖不合格的函数名称。

简而言之,如果您在函数调用的后缀表达式中使用非限定名称,您应该承认您已授予该函数可以“重写”的能力" 其他地方(是的,这是一种静态多态性)。因此,C++ 中函数的非限定名称的拼写正是接口的一部分。

在接受答案的情况下,如果 print_n 确实需要 ADL print (即允许它被覆盖),则应该使用不合格的 对其进行记录>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 不遵守异常规范)。这完美地达到了我的目的,叹息......

#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
}

尝试 https://wandbox.org/permlink/4pcqdx0yYnhhrASi 并将 USE_MY_SWAP_TEMPLATE 设置为 true 以查看歧义。

更新2018-11-05:

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

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

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>;

...所以它可以使用 我默认实现的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 ADL print (i.e. allowing it to be overridden), it should have been documented with the use of unqualified print as an explicit notice, thus clients would receive a contract that print should be carefully declared and the misbehavior would be all of the responsibility of my_stuff. Otherwise, it is a bug of print_n. The fix is simple: qualify print with prefix utility::. This is indeed a bug of print_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 declared swap function template in my namespace. Such swap would always found together with std::swap introduced by a idiomatic using std::swap; to use ADL under the ADL rules, and then there would come ambiguity of swap where the swap template (which would instantiate is_nothrow_swappable to get the proper noexcept-specification) is called. Combined with 2-phase lookup rules, the order of declarations does not count, once the library header containing the swap template is included. So, unless I overload all my library types with specialized swap function (to supress any candidate generic templates swap being matched by overloading resolution after ADL), I cannot declare the template. Ironically, the swap template declared in my namespace is exactly to utilize ADL (consider boost::swap) and it is one of the most significant direct client of is_nothrow_swappable in my library (BTW, boost::swap does not respects the exception specification). This perfectly beat my purpose up, sigh...

#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
}

Try https://wandbox.org/permlink/4pcqdx0yYnhhrASi and turn USE_MY_SWAP_TEMPLATE to true 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:

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>;

... 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 class MessageQueue. 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 a std name as a template argument, so the template would be looked up per ADL rules in the class scope. Thus, it finds std::multimap, which differs to the result of lookup in as the actual base class declared in the enclosing namespace scope. Since std::multimap uses std::allocator instance as the default template argument, BaseType is not the same type to the actual base class which have an instance of polymorphic_allocator, even multimap declared in the enclosing namespace is redirected to std::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 (where string is my own one being adapted to ISO C++17's string_view change, not std one in pre-C++17 modes). I would not figure out the bug is about ADL so quickly.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文