TL;DR: Can I expect that the code below will compile on any c++17 conformant c++ toolchain (based on the current c++17 proposal) and the failure of MSVC to do so is a bug in their implementation?
#include <string_view>
struct Foo : std::string_view {};
int main() {
Foo f1{};
Foo f2{};
return f1 == f2;
}
Explanation:
I have a class that is derived from std::string_view
and doesn't implement its own comparison operators, because the std::string_view
semantics are exactly what I need and I also want it to be comparable to e.g. a std::string
.
However, if I try to compare two instances of that class, MSVC 2017 complains about multiple overloads with similar conversions:
example.cpp
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/xlocale(314): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc
8 : <source>(8): error C2666: 'std::operator ==': 3 overloads have similar conversions
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/exception(336): note: could be 'bool std::operator ==(const std::exception_ptr &,const std::exception_ptr &) throw()' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/exception(341): note: or 'bool std::operator ==(std::nullptr_t,const std::exception_ptr &) throw()' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/exception(346): note: or 'bool std::operator ==(const std::exception_ptr &,std::nullptr_t) throw()' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/system_error(362): note: or 'bool std::operator ==(const std::error_code &,const std::error_code &) noexcept' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/system_error(370): note: or 'bool std::operator ==(const std::error_code &,const std::error_condition &) noexcept' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/system_error(378): note: or 'bool std::operator ==(const std::error_condition &,const std::error_code &) noexcept' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/system_error(386): note: or 'bool std::operator ==(const std::error_condition &,const std::error_condition &) noexcept' [found using argument-dependent lookup]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/xstring(970): note: or 'bool std::operator ==<char,std::char_traits<char>>(const std::basic_string_view<char,std::char_traits<char>>,const std::basic_string_view<char,std::char_traits<char>>) noexcept'
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/xstring(980): note: or 'bool std::operator ==<char,std::char_traits<char>,Foo&,void>(_Conv,const std::basic_string_view<char,std::char_traits<char>>) noexcept(<expr>)'
with
[
_Conv=Foo &
]
/opt/compiler-explorer/windows/19.10.25017/lib/native/include/xstring(990): note: or 'bool std::operator ==<char,std::char_traits<char>,Foo&,void>(const std::basic_string_view<char,std::char_traits<char>>,_Conv) noexcept(<expr>)'
with
[
_Conv=Foo &
]
8 : <source>(8): note: while trying to match the argument list '(Foo, Foo)'
Microsoft (R) C/C++ Optimizing Compiler Version 19.10.25017 for x64
Copyright (C) Microsoft Corporation. All rights reserved.
Compiler exited with result code 2
I do not know, why the the first few overloads (e.g. with std::error_code
) are listed at all. As the error message itself only talks about 3 overloads I guess they are only there for completeness, but are not part of the problem.
What confuses me however are those two overloads:
bool std::operator ==<char,std::char_traits<char>,Foo&,void>(_Conv,const std::basic_string_view<char,std::char_traits<char>>) noexcept(<expr>)
bool std::operator ==<char,std::char_traits<char>,Foo&,void>(const std::basic_string_view<char,std::char_traits<char>>,_Conv) noexcept(<expr>)
I could not find any mention of them on cppreference.com
and the code compiles fine under clang and gcc: https://godbolt.org/g/4Lj5qv, so they are probably not present in their implementation.
So my question is
EDIT:
Just for reference, the actual Foo
is an immutable string class very similar to this one: https://codereview.stackexchange.com/questions/116010/yet-another-immutable-string, but in order to simplify the design I wanted to replace my hand-rolled str_ref
with std::string_view
Yes you should expect your code to work; template argument deduction can deduce the base class in function calls, see [temp.deduct.call]/4.3
— If
P
is a class andP
has the form simple-template-id, then the transformedA
can be a derived class of the deducedA
.
The issue with VS 2017 (15.3) is - the standard also has provisions for situations when one of the arguments is implicitly-convertible to std::string_view
, see [string.view.comparison]:
Let
S
bebasic_string_view<charT, traits>
, andsv
be an instance ofS
. Implementations shall provide sufficient additional overloads markedconstexpr
andnoexcept
so that an objectt
with an implicit conversion toS
can be compared according to Table 67.Table 67 — Additional
basic_string_view
comparison overloads
- Expression
t == sv
equivalent to:S(t) == sv
- Expression
sv == t
equivalent to:sv == S(t)
- . . .
[ Example: A sample conforming implementation for
operator==
would be:template<class T> using __identity = decay_t<T>; template<class charT, class traits> constexpr bool operator==(basic_string_view<charT, traits> lhs, basic_string_view<charT, traits> rhs) noexcept { return lhs.compare(rhs) == 0; } template<class charT, class traits> constexpr bool operator==(basic_string_view<charT, traits> lhs, __identity<basic_string_view<charT, traits>> rhs) noexcept { return lhs.compare(rhs) == 0; } template<class charT, class traits> constexpr bool operator==(__identity<basic_string_view<charT, traits>> lhs, basic_string_view<charT, traits> rhs) noexcept { return lhs.compare(rhs) == 0; }
— end example ]
This causes a problem in VS 2017 (15.3) because:
The MSVC compiler can't handle partial ordering of function templates w.r.t. non-deduced context (thanks @T.C.), so the implementation mentioned in the standard is not possible
Consequently, the MSVC standard library works around that with SFINAE for overloads #2 and #3, see xstring
:
template<class _Elem, class _Traits, class _Conv, // TRANSITION, VSO#265216 class = enable_if_t<is_convertible<_Conv, basic_string_view<_Elem, _Traits>>::value>> _CONSTEXPR14 bool operator==(_Conv&& _Lhs, const basic_string_view<_Elem, _Traits> _Rhs) _NOEXCEPT_OP(_NOEXCEPT_OP((basic_string_view<_Elem, _Traits>(_STD forward<_Conv>(_Lhs))))) { // compare objects convertible to basic_string_view instances for equality return (_Rhs._Equal(_STD forward<_Conv>(_Lhs))); } template<class _Elem, class _Traits, class _Conv, // TRANSITION, VSO#265216 class = enable_if_t<is_convertible<_Conv, basic_string_view<_Elem, _Traits>>::value>> _CONSTEXPR14 bool operator==(const basic_string_view<_Elem, _Traits> _Lhs, _Conv&& _Rhs) _NOEXCEPT_OP(_NOEXCEPT_OP((basic_string_view<_Elem, _Traits>(_STD forward<_Conv>(_Rhs))))) { // compare objects convertible to basic_string_view instances for equality return (_Lhs._Equal(_STD forward<_Conv>(_Rhs))); }
Unfortunately this is not the same as what was meant in the standard - since the signature of these overloads differs from the original one, and Foo&&
is a better match than std::string_view
(again thanks @T.C.), no partial ordering between #1, #2 and #3 is performed - overload resolution selects #2 and #3 as better candidates. Now these two are truly ambiguous - both are viable yet neither is more specialized.
As a workaround you can implement comparators for your types, or just a generic comparator for when both sides are convertible to string_view
:
#include <string_view>
template<class T, class T2,
class = std::enable_if_t<std::is_convertible<T, std::string_view>::value>,
class = std::enable_if_t<std::is_convertible<T2, std::string_view>::value>>
constexpr bool operator==(T&& lhs, T2&& rhs) noexcept
{
return lhs.compare(std::forward<T2>(rhs));
}
struct Foo : std::string_view {};
int main() {
Foo f1{};
Foo f2{};
return f1 == f2;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With