c++gccsfinae

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.


Solution

  • 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)

    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