I have the following code that compiles fine on GCC 13.2. Colleagues can't compile it on GCC 13.1. The new version compiles fine even with -Wall -Wextra
. I assume that GCC 13.2 is better and more correct. Which change made this possible? Why is this call not ambiguous when it was considered ambiguous before?
Godbolt: https://godbolt.org/z/e9qKa4eza
The code allows casting to T*
, which in case of char*
allows the construction of the string without using the operator std::string
. Thus, StringifiedData<char>
effectively bypasses the string creation via the StringStream:
#include <string>
#include <sstream>
#include <algorithm>
template <typename T>
class StringifiedData
{
private:
size_t m_size{};
char * m_data{nullptr};
public:
StringifiedData(char * buffer)
: m_size{*reinterpret_cast<size_t*>(buffer)}, m_data{buffer + sizeof(size_t)}
{}
~StringifiedData() = default;
size_t size() const { return m_size; }
T * begin() const { return reinterpret_cast<T *>(m_data); }
T * end() const { return begin() + m_size; }
operator T *() const { return begin(); }
explicit operator std::string() const
{
std::stringstream sstr;
sstr << m_size;
std::for_each(begin(), end(), [&sstr](auto data){ sstr << "," << data; });
return sstr.str();
}
};
#include <iostream>
int main()
{
char buffer[128];
*reinterpret_cast<size_t*>(buffer) = 3;
StringifiedData<char> uCData(buffer);
uCData[0] = 'a';
uCData[1] = 'b';
uCData[2] = 'c';
std::cout << uCData.size() << ',' << uCData[0] << ',' << uCData[1] << ',' << uCData[2] << std::endl;
std::cout << static_cast<std::string>(uCData) << "\n" << std::endl;
}
Error message of GCC 13.1:
<source>:41:49: error: call of overloaded 'basic_string(StringifiedData<char>&)' is ambiguous
41 | std::cout << static_cast<std::string>(uCData) << "\n" << std::endl;
| ^
In file included from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/string:54,
from <source>:1:
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/basic_string.h:678:7: note: candidate: 'std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
678 | basic_string(basic_string&& __str) noexcept
| ^~~~~~~~~~~~
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/basic_string.h:642:7: note: candidate: 'std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
642 | basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/basic_string.h:548:7: note: candidate: 'std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
548 | basic_string(const basic_string& __str)
| ^~~~~~~~~~~~
Compiler returned: 1
Other notes:
-std=c++17
and then uses operator std::string
You are compiling with -std=c++14
. Emulation of old language modes in compilers is only approximate, especially since new DRs are discovered constantly against old standard modes.
The change that was made in GCC 13.2 that affects the behaviour of this code appears to be 9872d56 and it corresponds to the proposed resolution to CWG2735. In GCC 13.1 you are seeing an ambiguity between the constructors string(string&&)
and string(const char*)
because the first one is viable through the explicit operator std::string
and the second through the implicit operator T*
. The tweak in the above commit disfavours string(string&&)
because it's a move constructor that uses an explicit user-defined conversion in order to initialize its parameter. But I think that commit went beyond the intent, because it was only supposed to affect list-initialization, and only in cases where the ambiguity involves a candidate that would lead to an ill-formed program if chosen anyway. In your code, the situation is different because we're in a context where an implicit conversion sequence is actually allowed to call an explicit function. See the special rule for direct-initialization in [over.match.copy]/1.2. So arguably this is a bug in GCC 13.2 and later.
Note that in C++17 and later GCC implements a possible resolution to CWG2327 that makes string(string&&)
the best constructor because it's treated as if the user just called operator std::string
directly instead of calling string(const char*)
which requires a user-defined conversion (calling operator T*
) to initialize its parameter.