4

The following code compiles fine with GCC 13 and earlier, but GCC 14.1 produces an "ambiguous overload" error. Is it a compiler or code problem, and, more pragmatically, can I make the compiler prefer the non-member template by making changes in namespace ns (staying in the c++11 land)?

#include <iostream>
#include <sstream>
#include <string>

//=======================================================================
namespace ns {
  struct B
  {
    std::ostringstream os_;

    ~B()
    {
      std::cerr << os_.str() << '\n';
    }
  
    template<typename T>
    B& operator<<(T const& v)
    {
      this->f(v);
      this->os_ << ' ';
      return *this;
    }

  private:
    void f(int v) { os_ << v; }
    void f(std::string const& v) { os_ << "\"" << v << '\"'; }
  };

  struct D : public B
  {};
}
//==============================================================
namespace nsa {
  struct A
  {
    int i;
    std::string s;
  };

  template<typename S>
  S& operator<<(S&& s, A const & a)
  {
    s << "S<<A" << a.i << a.s; 
    return s;
  }
}
//==============================================================
int main()
{
  ns::D() << "XX" << nsa::A{1, "a"};
}

GCC 13 compiles it successfully and the program output is

"XX" "S<<A" 1 "a"  

The GCC 14 compiler output:

In function 'int main()':
<source>:50:19: error: ambiguous overload for 'operator<<' (operand types are 'ns::B' and 'nsa::A')
   50 |   ns::D() << "XX" << nsa::A{1, "a"};
      |       ~~~~~~~~~~~ ^~      ~~~~~~~~~
      |           |               |
      |           ns::B           nsa::A
<source>:17:8: note: candidate: 'ns::B& ns::B::operator<<(const T&) [with T = nsa::A]'
   17 |     B& operator<<(T const& v)
      |        ^~~~~~~~
<source>:41:6: note: candidate: 'S& nsa::operator<<(S&&, const A&) [with S = ns::B&]'
   41 |   S& operator<<(S&& s, A const & a)
      |      ^~~~~~~~

I thought the absence of other B::f() would lead to a substitution failure, taking the member operator<<() template out of the overload set. Multiple versions of clang think that it's an ambiguous overload. MSVC seems to try to convert A to an int or to a string as if it doesn't see the non-member template at all, and outputs something like

<source>(22): error C2664: 'void ns::B::f(const std::string &)': cannot convert argument 1 from 'const T' to 'int'
        with
        [
            T=nsa::A
        ]
<source>(22): note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
<source>(53): note: see reference to function template instantiation 'ns::B &ns::B::operator <<<nsa::A>(const T &)' being compiled
        with
        [
            T=nsa::A
        ]

The answer to this question seems to explain the ambiguity, although there are no calls to non-existing functions there, so I would not expect SFINAE to kick in in that example.
Adding an enable_if to the member template does not seem to work very well, because there can be types convertible to int for which one may want to optionally define a non-member template.

4
  • The error is pretty straightforward and is due to same reason as explain in your linked question. But here you want to call the non-member template<typename S> S& operator<<(S&& s, A const & a) Commented Jul 6 at 14:35
  • "can I make the compiler prefer the non-member template by making changes in namespace ns..." Yes, one way of doing the ambiguity is by removing the low-level const from the parameter of ns::B::operator<<. Demo Commented Jul 6 at 14:42
  • Sidenote: There is also another way of removing ambiguity(by making T const& v to T&& v) but that will use the member template which is not what you want. So in my answer, if you remove change T const& v to T & v then you'll have your desired behavior. Commented Jul 6 at 14:49
  • 1
    "I thought the absence of other B::f() would lead to a substitution failure": SFINAE considers only the declaration of the functions that are considered for overload resolution. It doesn't decide anything based on the body of the function. There is nothing in C++ to let the compiler check the validity of a function body. Commented Jul 6 at 15:09

2 Answers 2

2

Is it a compiler or code problem?

A code problem.

Diagnostic of gcc 14 is pretty clear With

ns::D() << "XX" << nsa::A{1, "a"};
//              ^^

We have "ns::B& << nsa::A" (ns::D is "lost" with ns::D() << "XX" which returns ns::B)

and we have 2 equally overloads (exact match)

  • ns::B::operator<< (const T&) with T == nsa::A
  • nsa::operator<< (T&&, const A&) with T == ns::B&

neither is more specialized than the other.

SFINAE doesn't happens on body.

You might apply SFINAE, which remove ns::B::operator<< from viable function, removing ambiguity for your case:

template<typename T>
auto operator<<(T const& v)
-> decltype(this->f(v), *this)
{
  this->f(v);
  this->os_ << ' ';
  return *this;
}

Demo

6
  • If understand it correctly, you put the expression that causes substitution failure in the declaration, but discard it's type if substitution succeeds. Ingenious! Unfortunately, I am looking for a C++11 solution.
    – akryukov
    Commented Jul 6 at 22:58
  • 1
    Fortunately, it is C++11, (as you might see in demo.
    – Jarod42
    Commented Jul 6 at 23:13
  • Yes, indeed. It works quite well, except in the case when there is an enum with non-member operator<<(), as it is convertible to int. namespace nsa { enum E { E0, E1 }; template <typename S> operator<<(S&& s, E e) { switch(e) { case E0: s << "E0"; break; case E1: s << "E1"; break; default: s << "EX"; break; } return s; } } ... ns::D() << "Enum" << nsa::E_0; It works in both MSVC ang GCC before 14, but in GCC 14 it's still ambiguous. Any suggestions how to handle this?
    – akryukov
    Commented Jul 7 at 23:49
  • SFINAE also on is_enum: Demo
    – Jarod42
    Commented Jul 8 at 1:01
  • That approach occurred to me, but it doesn't work if only some enums have a custom operator<<() template, and others are happy with just being converted to int.
    – akryukov
    Commented Jul 8 at 13:05
0

can I make the compiler prefer the non-member template by making changes in namespace ns

Yes, you can remove the ambiguity by removing the low-level const from ns::B::operator<<'s parameter as shown below. This will make the non-member template a better match than the member version of operator<<.

Basically, member functions are considered to have an implicit object parameter for overload resolution purposes. This makes both of the version in your example to have same rank(no one is better/worse than other). The same is explained in Templated operator overload resolution, member vs non-member function

namespace ns {
  struct B
  {
    //other code as before
  
    template<typename T>
    //-------------vv------>removed low-level const from here
    B& operator<<(T & v)   
    {
      this->f(v);
      this->os_ << ' ';
      return *this;
    }

  //other code as before
}

Working demo


Note that making it non-const implies that changes can be made to v inside the function. Also, the ambiguity is back if the user tries to use the operator with a const qualified T.

3
  • 1
    @TedLyngmo Added that at the end of the answer. Btw I changed "will be" to "can be" in your comment when adding it at the end of the answer. Since in the given example they don't intend to change the data member. Commented Jul 6 at 14:54
  • Removing const doesn't seem like the right thing to do, because it will disallow code like ns::D() << 2; or nsa::A a{1, "a"}; ns::D() << a.getInt();, assuming the int getInt() const function is added to A
    – akryukov
    Commented Jul 6 at 22:33
  • And nsa::A a{1, "a"}; ns::D() << a; fails to compile with MSVC and generates a ton of warnings in GCC.
    – akryukov
    Commented Jul 6 at 22:40

Not the answer you're looking for? Browse other questions tagged or ask your own question.