Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Comparison for objects derived from std::string_view is ambiguous in MSVC

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

  • Is their existence actually allowed (or even mandated) by the expected c++17 standard or is that a bug in MSVC?
  • If something like this is allowed in a standard conforming c++ standard library, is there a simple workaround that doesn't require me to implement all the comparators myself (I know, they are trivial to write, but it should imho not be necessary and I'd have to repeat the process for multiple types).

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

like image 224
MikeMB Avatar asked Aug 18 '17 08:08

MikeMB


1 Answers

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 and P has the form simple-template-id, then the transformed A can be a derived class of the deduced A.

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 be basic_­string_­view<charT, traits>, and sv be an instance of S. Implementations shall provide sufficient additional overloads marked constexpr and noexcept so that an object t with an implicit conversion to S 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;
}
like image 136
rustyx Avatar answered Sep 30 '22 16:09

rustyx