March 9th, 2026
like1 reaction

Learning to read C++ compiler errors: Ambiguous overloaded operator

A customer was adding a feature to an old C++ code base, and the most convenient way to consume the feature was to use C++/WinRT. However, once they added C++/WinRT to their project, they ran into compiler errors in parts of their code that hadn’t changed in decades. As an added wrinkle, the problem occurred only in 32-bit builds.

std::ostream& operator<<(ostream& os, LARGE_INTEGER const& value)
{
    return os << value.QuadPart; // ← error
}

The error complained that the << operator was ambiguous.

contoso.cpp(3141) : error C2593: 'operator <<' is ambiguous
ostream(436): note: could be 'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(long double)'
ostream(418): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(double)'
ostream(400): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(float)'
ostream(382): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned __int64)'
ostream(364): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(__int64)'
ostream(346): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned long)'
ostream(328): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(long)'
ostream(309): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned int)'
ostream(283): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(int)'
ostream(264): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned short)'
ostream(230): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(short)'
ostream(212): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(bool)'
contoso.h(1554): note: or       'std::ostream &operator <<(std::ostream &,const unsigned __int64 &)'
contoso.h(1548): note: or       'std::ostream &operator <<(std::ostream &,const __int64 &)'
ostream(953): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::operator <<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,unsigned char)'
ostream(942): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::operator <<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,signed char)'
ostream(819): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::operator <<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char)'
ostream(738): note: or       'std::basic_ostream<char,std::char_traits<char>> &std::operator <<<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char)'
contoso.cpp(1582): note: while trying to match the argument list '(std::ostream, LONGLONG)'

All of these are different overloads of std::ostream& std::ostream::operator<<(something) or std::ostream& operator<<(std::ostream&, something), so let’s remove all the repeated stuff to make it easier to see what we are up against.

ostream(436): long double
ostream(418): double
ostream(400): float
ostream(382): unsigned __int64
ostream(364): __int64
ostream(346): unsigned long
ostream(328): long
ostream(309): unsigned int
ostream(283): int
ostream(264): unsigned short
ostream(230): short
ostream(212): bool
contoso.h(1554): const unsigned __int64 &
contoso.h(1548): const __int64 &
ostream(953): unsigned char
ostream(942): signed char
ostream(819): char
ostream(738): char

From the code, we see that the intention is to use the insertion operator that takes a signed 64-bit integer, so let’s filter down to those.

ostream(364): __int64
contoso.h(1548): const __int64 &

Aha, now we see the conflict. The C++ standard library (<ostream>) has defined an output inserter for __int64, and the customer has defined an output inserter for const __int64&, so the compiler can’t choose between them.

The compiler kindly provided line numbers, so we can look at the conflict introduced by contoso.h.

#if !defined(_WIN64) && !defined(_STL70_) && !defined(_STL110_)

// These are already defined in STL
std::ostream& operator<<(std::ostream&, const __int64& );
std::ostream& operator<<(ostd::stream&, const unsigned __int64& );

#endif /* _WIN64 _STL70_ _STL110_ */

Okay, well the !defined(_WIN64) explains why this problem occurs only in 32-bit builds: The conflicting definition is #if‘d out in 64-bit builds.

The rest of the #if expression removes the conflicting definition for STL versions 7.0 and 11.0. So what happened to reactivate this code path?

Adding C++/WinRT to the project.

C++/WinRT requires C++17 or later, which means that the project had to bump its compiler version, and that pushed the STL version to 12.0. And their custom #if doesn’t handle that case.

I went back through the project history, and saw that about five years ago, the line was just

#if !defined(_WIN64) && !defined(_STL70_)

So I’m guessing that at some point in the past five years, they upgraded their compiler version, and they ran into this exact problem, and they realized, “Oh, we need to suppress this for STL 11.0, too,” and they added the !defined(_STL110_).

History repeats itself.

One solution is to put another layer of duct tape on it.

#if !defined(_WIN64) && !defined(_STL70_) && !defined(_STL110_) && !defined(_STL120_)

Of course, this just means that in another five years, when they decide to upgrade to C++30, this problem will come back and somebody will have to add yet another layer of duct tape.

So they could choose something that is a bit more forward-compatible:

#if !defined(_WIN64) && !defined(_STL70_) && !defined(_STL110_) && __cplusplus < 201700

Or they could just delete the entire block. I doubt they are going to roll their compiler back to C++03.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

1 comment

Sort by :
  • LB 22 minutes ago

    Always feels great when the solution is to simply delete code.