Каковы подводные камни ADL?

Некоторое время назад я прочитал статью, в которой объяснялось несколько ошибок, зависящих от аргументов, но я больше не могу их найти. Речь шла о том, чтобы получить доступ к вещам, к которым у вас не должно быть доступа или что-то в этом роде. Поэтому я подумал, что я бы спросил здесь: каковы подводные камни ADL?

Ответ 1

Существует огромная проблема с зависящим от аргумента поиска. Рассмотрим, например, следующую утилиту:

#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 в действии.)

Да, зависящий от аргументов поиск является важной особенностью С++. По существу требуется добиться желаемого поведения некоторых языковых функций, таких как перегруженные операторы (рассмотрим библиотеку потоков). Тем не менее, это также очень, очень ошибочно и может привести к действительно уродливым проблемам. Было несколько предложений по исправлению зависимого от аргументов поиска, но ни один из них не был принят комитетом стандартов С++.