15

I saw both std::string_view and std::string have symmetric operator==() and for std::string it has the constructor to accept std::string_view and operator to convert itself to std::string_view. So when we try to use operator==() compare between std::string_view and std::string, is it supposed to be ambiguous?

I think there must be something wrong with my thoughts. Can anyone clarify?

Example:

std::string s1 = "123";
std::string_view s2 = "123";
// in the following comparison, will s1 use the convert operator to generate a string_view, or will s2 use string's string_view constructor to generate a string?
if (s1 == s2) {...}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
golden retriever
  • 297
  • 1
  • 2
  • 10
  • 4
    Can you provide a code example where it would be ambiguous? – Nicol Bolas Sep 12 '18 at 00:58
  • I guess the OP is wondering whether `string_view` or `string`'s `op==` will be used in the case that there is one of each in the args. – Lightness Races in Orbit Sep 12 '18 at 15:06
  • @LightnessRacesinOrbit: Does it matter? They both do the same thing, so which gets called is essentially sophistry. And even if we're going to engage in sophistry, it depends on the order of the parameters, so without an actual example the question cannot be answered. – Nicol Bolas Sep 12 '18 at 15:55
  • 6
    @NicolBolas Well, it's not sophistry, because if two functions are an exact match for some call, the compiler will error out with an ambiguity. It doesn't check them for semantic equivalence first then let you get away with it. – Lightness Races in Orbit Sep 12 '18 at 15:57
  • I did have a quick look at the standard trying to come up with a quick answer to this, but said answer is not immediately obvious to me. I think this is a reasonable question. – Lightness Races in Orbit Sep 12 '18 at 15:58
  • @LightnessRacesinOrbit: *What* is a reasonable question? There's no example code here to actually talk about here, merely speculation about something being "ambiguous". We can only guess about what the specific circumstances the OP things is ambiguous. – Nicol Bolas Sep 12 '18 at 16:01
  • 2
    @NicolBolas Granted it could use some clarification and expansion, but I mean fundamentally, I don't think it's a pointless or vacuous question as you seem to be suggesting? – Lightness Races in Orbit Sep 12 '18 at 16:35
  • Sorry about the confusion. I have updated an example. – golden retriever Sep 12 '18 at 16:41

2 Answers2

15

The reason such a comparison cannot be ambiguous is that neither std::string nor std::string_view are plain types. Instead, these are class templates instantiations, and so are respective comparison operators:

template <class charT, class traits, class alloc>
constexpr bool operator==(const basic_string<charT, traits, alloc>& lhs,
                          const basic_string<charT, traits, alloc>& rhs) noexcept;

template <class charT, class traits>
constexpr bool operator==(basic_string_view<charT, traits> lhs,
                          basic_string_view<charT, traits> rhs) noexcept;

Such defined function templates do not consider any conversions. Instead, they expect the operands to be of exactly the same type, as only then the deduction succeeds (the same types can be deduced for template parameters of left and right operands), producing a viable candidate. Similarly:

template <typename T>
void foo(T, T);

foo(42, 'x'); // error

fails due to mismatch of types of arguments, as T cannot be either int or char, although conversions between the two exist. Also:

struct my_string
{
    operator std::string() const { return ""; }
};

std::string s;
my_string ms;
s == ms; // error

fails, because the compiler cannot deduce basic_string<charT, traits, alloc> from my_string, although there does exists an implicit conversion to its instantiation.

The comparison s1 == s2 does, however, work, because the implementation of the standard library is expected to provide overloads that can consider implicit conversions from any type to std::basic_string_view (such an implicit conversion exists from std::string to std::string_view). This can be achieved, e.g., by inhibiting deduction for one of the parameters, as shown in the example part of [string.view.comparison]/p1:

template <class charT, class traits>
constexpr bool operator==(basic_string_view<charT, traits> lhs,
                          __identity<basic_string_view<charT, traits>> rhs) noexcept;

By putting the type of one of the operands in __identity defined as template <class T> using __identity = decay_t<T>;, it introduces a non-deduced context, creating an overload for some std::basic_string_view and another argument implicitly convertible to the same instantiation of the std::basic_string_view class template.

rturrado
  • 7,699
  • 6
  • 42
  • 62
Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
  • Thanks, i know it's probably out of this question's scope, but how `decay_t>` force an implicit conversion? – golden retriever Sep 12 '18 at 23:13
  • @goldenretriever `std::decay_t` is an alias template that expands to `typename std::decay::type`. the compiler, seeing that `::type` is a dependent type (depending on `T`) will not attempt to deduce `T`. it's a so called [non-deduced context](https://stackoverflow.com/a/25245676/3953764). however, if `T` is also used in a definition of another function parameter, and *can* be deduced there, then it will be used to instantiate that parameter that is in a non-deduced context. eventually, you do get `operator==` accepting two `basic_string_view`, but of template arguments from only one arg. – Piotr Skotnicki Sep 13 '18 at 05:05
  • @goldenretriever passing an argument to a function follows copy-initialization semantics which can use only implicit conversions – Piotr Skotnicki Sep 13 '18 at 05:06
5

This works because of an odd clause in [string.view.comparisons]:

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 62.

And Table 62 lists all of the comparison operators, with the view on either side of the expression.

Since std::string has an implicit conversion to std::string_view, it is this overload which will be chosen. Such overloads will have an exact match to the s1 == s2 case, so implicit conversions will not be considered.

Basically, this is implemented through SFINAE tools. Something like this:

template<typename Str>
std::enable_if_t<std::is_convertible_v<std::string_view, Str>, bool> operator==(const Str &rhs, const std::string_view &lhs);

Such an overload doesn't require implicit conversions, so it's better than any overload that does.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Hmm... the paragraph doesn't say that such an overload should generate an exact match, and the *"sample conforming implementation"* below is not based on a function template that would do so. – Piotr Skotnicki Sep 12 '18 at 18:02
  • @PiotrSkotnicki: "shall provide sufficient additional overloads ... so that an object `t` with an implicit conversion to `S` can be compared". If they're not exact matches, then the comparison would be ambiguous with `string_view` comparisons, and thus they could not be compared. The requirement of an exact match is therefore a requirement. Also, there's pretty much no way to implement such a thing *without* also making it an exact match, even ignoring the creation of an ambiguity. – Nicol Bolas Sep 12 '18 at 18:04
  • the comparisons can't be ambiguous with string_view comparison operators because each expects the exact same template arguments to be deduced for both operands. the sample conforming implementation puts one of the operands in a non-deduced context, forcing an implicit conversion to string_view at the call site – Piotr Skotnicki Sep 12 '18 at 18:24
  • @PiotrSkotnicki: I don't know what you mean. The sample code was explaining how you write a function that only works on a type that is implicitly convertible to `string_view`. It was not meant to be a whole and complete implementation of the standard's requirements for `operator==`. – Nicol Bolas Sep 12 '18 at 18:29
  • Thanks Nicol and Piotr. I think I understand that exact match will get rid of the ambiguity. But since I didn't find similar implementation mentioned at this website https://en.cppreference.com I usually get reference from, I was wondering would the point Piotr mentioned before that because the string constructor is marked as `explicit` so `operator==(string, string)` won't be selected, also be the reason? As it also makes sense to me. – golden retriever Sep 12 '18 at 20:07
  • 1
    @goldenretriever there is no `bool operator==(string, string)`. instead, there's `template bool operator==(const basic_string&, const basic_string&)`, which works only if deduction succeeds for both operands, and so it does not consider conversions, which is why my answer is deleted – Piotr Skotnicki Sep 12 '18 at 20:12
  • @NicolBolas this `__identity` from the sample implementation is not a type for which `operator==` is provided, it's a way of forcing an implicit conversion for one (arbitrary) of operands, and here's where I don't see any "exact match" that you mention – Piotr Skotnicki Sep 12 '18 at 20:19
  • @PiotrSkotnicki: I don't know what `__identity` you're talking about. – Nicol Bolas Sep 12 '18 at 20:41
  • Both answers do not explain the same thing. Which is annoying. The question also would need improvement, because what really matter is to make sure no (implicit) conversion from the string_view to a string is done . (would cause bad performance) – sandwood Dec 17 '21 at 12:59