c++c++17perfect-forwardingargument-dependent-lookup

Trouble with using overloaded << for std::variant


I have an overloaded << for an aliased std::variant (A::Var). I also have a templated function defined in a class in a different namespace C::Wrapper which just forwards its argument to an std::ostream.

I am trying to invoke it from another function defined within a class in A, A::Foo, but this is giving me compiler errors. This toy example is below.

#include <iostream>
#include <variant>

namespace C {
  struct Wrapper {
    template<typename T>
    auto& operator<<(T&& v) {
      std::cout << std::forward<T>(v);
      return *this;
    }
  };
}

namespace A {
  using Var = std::variant<bool, int>;

  auto& operator<<(std::ostream& os, const A::Var& v) {
    std::visit([&os](auto i) { os << i; }, v);
    return os;
  }

  struct Foo {
    void m() {
      C::Wrapper wrap;
      Var v{3};
      wrap << "hi"; // works
      wrap << v; // compiler error
    }
  };
}

int main() { 
  A::Foo a;
  a.m();
}

g++ -std=c++17 gives the following error:

main.cpp: In instantiation of ‘auto& C::Wrapper::operator<<(T&&) [with T = std::variant<bool, int>&]’:
main.cpp:27:11:   required from here
main.cpp:8:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘std::variant<bool, int>’)
    8 |     std::cout << std::forward<T>(v);

... many candidate functions none including my overloaded << for A::Var

I expected it to compile successfully and print 3 when run. I tried making all the definitions out of line, removing the const qualifier, making the overloaded << global, but none of those worked.

How can I fix this error, while keeping the namespace and class structure?


UPDATE:

The code compiles with gcc by defining << in the global namespace, like this, but it fails with clang:

namespace A {
  using Var = std::variant<bool, int>;
}

auto &operator<<(std::ostream &os, const A::Var &v) {
  std::visit([&os](auto i) { os << i; }, v);
  return os;
}

namespace A {
  struct Foo {
    void m() {
      C::Wrapper wrap;
      A::Var v{3};
      wrap << "hi"; // works
      wrap << v;    // compiler error
    }
  };
} // namespace A

Error with clang++ -std=c++17:

main.cpp:7:15: error: call to function 'operator<<' that is neither visible in the template definition nor found by argument-dependent lookup
    std::cout << std::forward<T>(v);
              ^
main.cpp:28:10: note: in instantiation of function template specialization 'C::Wrapper::operator<<<std::variant<bool, int> &>' requested here
    wrap << v;    // compiler error
         ^
main.cpp:17:7: note: 'operator<<' should be declared prior to the call site
auto &operator<<(std::ostream &os, const A::Var &v) {
      ^
1 error generated.

Why does it work with gcc, but not with clang?


Solution

  • Argument-dependent lookup (ADL) doesn't consider namespaces associated with aliases. (That wouldn't make sense either. Should the compiler remember all namespaces having an alias to std::variant<bool, int> and then suddenly consider all functions declared there as belonging to std::variant<bool, int>?)

    Make Var a proper independent type. Either you would make it a class with a private std::variant<bool, int> member and then you can implement and forward the parts of the interface of std::variant that you need, or you let Var inherit publicly from std::variant<bool, int> and inherit the constructors, which has the benefit that std::visit will still work out-of-the-box.

    Then ADL will take into account operator<< overloads in the namespace A when you apply << to a Var.