Question

C++ ambiguous member vs non-member function template resolution in GCC 14 but not in prior GCC versions

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  97  4
1 Jan 1970

Solution

 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

2024-07-06
Jarod42

Solution

 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.

2024-07-06
user12002570