Почему компилятор не может найти эту перегрузку оператора?

Я пытаюсь написать перегрузки operator<< для конкретных экземпляров контейнеров стандартной библиотеки, которые будут храниться в boost::variant. Вот небольшой пример, иллюстрирующий проблему:

#include <iostream>
#include <vector>

std::ostream & operator<<( std::ostream & os, const std::vector< int > & ) {
  os << "Streaming out std::vector< int >";
  return os;
}

std::ostream & operator<<( std::ostream & os, const std::vector< double > & ) {
  os << "Streaming out std::vector< double >";
  return os;
}

#include <boost/variant.hpp>

typedef boost::variant< std::vector< int >, std::vector< double > > MyVariant;

int main( int argc, char * argv[] ) {
  std::cout << MyVariant();
  return 0;
}

Первая ошибка Clang

boost/variant/detail/variant_io.hpp:64:14: error: invalid operands to binary expression ('std::basic_ostream<char>' and 'const std::vector<int, std::allocator<int>>')
        out_ << operand;
        ~~~~ ^  ~~~~~~~

Я понимаю, что #include <boost/variant.hpp> находится в странном месте. Я почти уверен, что проблема была связана с двухэтапным поиском имени в шаблонах, поэтому я переместил #include в попытке реализовать исправление №1 из документация clang по поиску. Исправление № 2 из этой документации не является хорошим вариантом, потому что я считаю, что добавление моего перегруженного оператора ‹‹ в пространство имен std приведет к неопределенному поведению.

Не следует ли определять мои operator<< перед #include, чтобы компилятор мог найти определения? Эта техника работает в следующем примере, взятом с той же страницы clang.

#include <iostream>

namespace ns {
  struct Data {};
}

std::ostream& operator<<(std::ostream& out, const ns::Data & data) {
  return out << "Some data";
}

namespace ns2 {
  template<typename T>
  void Dump( std::ostream & out, const T & value) {
    out << value;
  }
}

int main( int argc, char * argv[] ) {
  ns2::Dump( std::cout, ns::Data() );
}

person RecursiveDescent    schedule 15.09.2013    source источник


Ответы (1)


Во время создания экземпляра шаблона шаблон функции, зависящий от типа шаблона, обнаруживается только во время поиска фазы II. Поиск на этапе II не рассматривает имена, видимые в момент использования, а рассматривает только имена, найденные на основе поиска, зависящего от аргумента. Поскольку единственным связанным пространством имен для std::ostream и std::vector<int> является пространство имен std, он не ищет ваши операторы вывода, определенные в глобальном пространстве имен. Конечно, вам не разрешено добавлять эти операторы в пространство имен std, что является настоящим уловом: вы можете определять эти операторы только для контейнеров, включающих, по крайней мере, один определяемый пользователем тип! Возможным способом обойти это ограничение является добавление пользовательского распределителя, который просто является производным от std::allocator<T>, но находится в подходящем определяемом пользователем пространстве имен: затем вы можете определить операторы вывода в этом пространстве имен. Недостатком этого подхода является то, что std::vector<T> (т. е. без параметра распределителя) в значительной степени является типом словаря.

Перемещение объявлений не помогает: поиск имени фазы II на самом деле не зависит от порядка объявления, за исключением того, что объявления должны предшествовать точке инстанцирования. Единственное правильное решение состоит в том, чтобы определить операторы в пространстве имен, которое ищется при поиске фазы II, что в значительной степени означает, что печатаемые типы должны включать пользовательский тип.

person Dietmar Kühl    schedule 15.09.2013
comment
быстрое и довольно грязное исправление состоит в том, чтобы сделать struct vI: std::vector<int> {}; и тому подобное для другого вектора, а затем код делает то, что хочет OP, потому что эти типы живут в глобальном пространстве имен. - person TemplateRex; 16.09.2013
comment
@TemplateRex: Согласен. ... а с С++ 11 тип можно сделать достаточно полным, используя template <typename T> struct myvector: std::vector<int> { using std::vector<T>::vector; };, поскольку объявление using приводит к наследованию конструкторов std::vector<T>. - person Dietmar Kühl; 16.09.2013
comment
или вариативный псевдоним шаблона, чтобы также учитывать распределители. Ах, мое королевство для непрозрачных определений типов :-) - person TemplateRex; 16.09.2013
comment
Если это так, то почему этот пример компилируется без ошибок? #include <iostream> namespace ns { struct Data {}; } std::ostream& operator<<(std::ostream& out, const ns::Data & data) { return out << "Some data"; } namespace ns2 { template<typename T> void Dump( std::ostream & out, const T & value) { out << value; } } int main( int argc, char * argv[] ) { ns2::Dump( std::cout, ns::Data() ); } редактировать: не знаю, как лучше форматировать код в комментариях, извините. Я отредактирую исходный вопрос с помощью этого примера. - person RecursiveDescent; 16.09.2013
comment
@ user2781966: Кажется, я немного увлекся: 14.6.4 [temp.dep.res] действительно указывает, что имена, видимые в точке определения шаблона, считаются разрешающими зависимые имена. Однако полагаться на порядок объявлений проблематично, так как это может легко привести к поиску разных функций в разных единицах перевода, что приведет к нарушению правила одного определения (3.4 [basic.def.odr]): кажется, я перепутал то, что, как я понял, является разумным подходом к перегрузке операторов, используемых шаблонами, с тем, что на самом деле требует стандарт. - person Dietmar Kühl; 16.09.2013
comment
Затем, на основе 14.6.4, мой первый пример должен скомпилироваться без ошибок (хотя я согласен полагаться на порядок объявлений — это плохо). Может быть, это ошибка в clang и gcc, так как они оба отвергают мой первый пример? - person RecursiveDescent; 16.09.2013
comment
@user2781966: user2781966: Кажется, он должен скомпилироваться, но, возможно, мое исходное утверждение было не таким уж неверным. Это, безусловно, помогает использовать myvector<int>, как в комментарии выше. - person Dietmar Kühl; 16.09.2013