Почему компилятор выбирает неправильную перегрузку функции в этом случае?

Я пробовал код, представленный Шон Ройент в его разговоре на GoingNative 2013 - "Наследование - это базовый класс зла" . (код от последнего слайд доступен в https://gist.github.com/berkus/7041546

Я попытался достичь одной и той же цели самостоятельно, но я не могу понять, почему приведенный ниже код не будет действовать, как я ожидаю.

#include <boost/smart_ptr.hpp>
#include <iostream>
#include <ostream>

template <typename T>
void draw(const T& t, std::ostream& out)
{
    std::cout << "Template version" << '\n';
    out << t << '\n';
}

class object_t
{
public:
    template <typename T>
    explicit object_t (T rhs) : self(new model<T>(rhs)) {};

    friend void draw(const object_t& obj, std::ostream& out)
    {
        obj.self->draw(out);
    }

private:
    struct concept_t
    {
        virtual ~concept_t() {};
        virtual void draw(std::ostream&) const = 0;
    };

    template <typename T>
    struct model : concept_t
    {
        model(T rhs) : data(rhs) {};
        void draw(std::ostream& out) const
        {
            ::draw(data, out);
        }

        T data;
    };

    boost::scoped_ptr<concept_t> self;
};

class MyClass {};

void draw(const MyClass&, std::ostream& out)
{
    std::cout << "MyClass version" << '\n';
    out << "MyClass" << '\n';
}

int main()
{
    object_t first(1);
    draw(first, std::cout);

    const object_t second((MyClass()));
    draw(second, std::cout);

    return 0;
}

Эта версия обрабатывает печать int отлично, но не выполняет компиляцию во втором случае, поскольку компилятор не знает, как использовать MyClass с operator<<. Я не могу понять, почему компилятор не будет выбирать вторую перегрузку, предоставленную специально для MyClass. Код компилируется и отлично работает, если я изменяю имя метода model:: draw() и удаляю из его тела спецификатор пространства имен ::, или если я изменяю глобальную функцию рисования MyClass на полную специализацию шаблона.

Сообщение об ошибке, которое я получаю, приведено ниже, после чего это куча candidate function not viable...

t76_stack_friend_fcn_visibility.cpp:9:9: error: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'const MyClass')
    out << t << '\n';
    ~~~ ^  ~
t76_stack_friend_fcn_visibility.cpp:36:15: note: in instantiation of function template specialization 'draw<MyClass>' requested here
            ::draw(data, out);
              ^
t76_stack_friend_fcn_visibility.cpp:33:9: note: in instantiation of member function 'object_t::model<MyClass>::draw' requested here
        model(T rhs) : data(rhs) {};
        ^
t76_stack_friend_fcn_visibility.cpp:16:42: note: in instantiation of member function 'object_t::model<MyClass>::model' requested here
    explicit object_t (T rhs) : self(new model<T>(rhs)) {};
                                         ^
t76_stack_friend_fcn_visibility.cpp:58:20: note: in instantiation of function template specialization 'object_t::object_t<MyClass>' requested here
    const object_t second((MyClass()));
                   ^

Почему шаблонная версия функции глобального шаблона дроби выбрана из-за перегрузки функции MyClass? Это потому, что ссылка на шаблон является жадной? Как исправить эту проблему?

Ответ 1

Потому что вы используете квалифицированное имя в вызове функции. [Temp.dep.candidate]:

Для вызова функции, который зависит от параметра шаблона, функции-кандидата найдены с использованием обычных правил поиска (3.4.1, 3.4.2, 3.4.3), за исключением того, что:

  • Для части поиска, использующей поиск неквалифицированного имени (3.4.1) или квалифицированный поиск имени (3.4.3), только объявления функций из контекст определения шаблона.
  • Для части поиска с использованием связанных пространств имен (3.4.2) только объявления функций, найденные в определении шаблона контекст или контекст экземпляра шаблона.

§3.4.2 (псевдоним [basic.lookup.argdep]):

Когда постфиксное выражение в вызове функции (5.2.2) является unqualified-id, другие пространства имен, не учитываемые при обычном неквалифицированном поиске (3.4.1), и в этих пространствах имен, объявления функции имени пространства имен (11.3) иначе видимый может быть найден.

Таким образом, по существу ADL не применяется, так как вызов использует идентификатор Qualified.
Как Барри показывает в своем ответе, вы можете решить эту проблему, сделав вызов неквалифицированным:

void draw(std::ostream& out) const
{
    using ::draw;
    draw(data, out);
}

Вы должны добавить using -declaration до этого. В противном случае поиск неквалифицированного имени найдет функцию-член model<>::draw при поиске в декларативных областях в порядке возрастания и больше не будет искать. Но не только это - , потому что model<>::draw (который является членом класса) найден мой неквалифицированный поиск имени, ADL не, [basic.lookup.argdep]/3:

Пусть X - набор поиска, созданный неквалифицированным поиском (3.4.1) и пусть Y - это набор поиска, созданный зависимым от аргумента поиска (определяется следующим образом). Если X содержит

  • объявление члена класса или
  • объявление функции блока-области, которое не является декларацией использования, или
  • объявление, которое не является ни функцией, ни шаблоном функции

то Y пусто. В противном случае Y - это набор объявлений, найденных в пространствах имен, связанных с типы аргументов, как описано ниже.

Следовательно, если предоставляется using -declaration, единственным объявлением, найденным при поиске неквалифицированного имени, будет глобальный шаблон draw, который был введен в декларативный регион model::draw. Затем вызывается ADL и находит более позднюю объявленную функцию draw для MyClass const&.

Ответ 2

При прямом вызове ::draw() вы не сможете правильно использовать ADL. (Почему? Я действительно не знаю конкретно и, надеюсь, кто-нибудь придет и объяснит это мне тоже. [Edit: см. ответ Колумба с тем, почему]) Но в порядке для фактического использования ADL вам нужно сделать безоговорочный вызов draw следующим образом:

void draw(std::ostream& out) const
{
    using ::draw;
    draw(data, out);
}

Это правильно обнаружит перегрузку draw(const MyClass&, std::ostream&).