C++17中比较string_view和string时的歧义问题

16

我看到 std::string_viewstd::string 都具有对称的 operator==(), 对于 std::string, 它有一个构造函数接受std::string_view和可以将自身转换为 std::string_view 的运算符。所以当我们尝试使用 operator==() 比较 std::string_viewstd::string 时,这是不是应该是模糊的?

我认为我的想法一定有问题。 有人能澄清吗?

例如:

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) {...}

4
可以提供一个代码示例,其中会产生歧义吗? - Nicol Bolas
我猜 OP 想知道如果参数中既有 string_view 又有 string,那么 op== 会使用哪一个。 - Lightness Races in Orbit
6
如果两个函数完全匹配某个调用,编译器会出现二义性错误,因此这并不是诡辩。它不会先检查它们是否语义等价,然后让你通过。@NicolBolas - Lightness Races in Orbit
我确实快速查看了标准,试图快速回答这个问题,但是答案对我来说并不是显而易见的。我认为这是一个合理的问题。 - Lightness Races in Orbit
2
@NicolBolas 虽然它可能需要一些澄清和扩展,但从根本上讲,我认为它并不像你所暗示的那样毫无意义或空洞。 - Lightness Races in Orbit
显示剩余3条评论
2个回答

16
这样的比较之所以不会产生歧义,是因为std :: stringstd :: string_view都不是普通类型。相反,它们都是类模板实例化,并且各自具有比较运算符:
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;

这样定义的函数模板不考虑任何转换。相反,它们期望操作数完全相同类型,只有这样才能成功推导(左右操作数的模板参数可以推导出相同类型),从而产生一个可行的候选项。同样地:

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

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

由于参数类型不匹配而导致失败,因为T既不能是int也不能是char,尽管两者之间存在转换。此外:

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

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

使用my_string无法推断出basic_string<charT, traits, alloc>的类型,因此会出现错误。但是s1 == s2可以工作,因为标准库的实现预计提供重载函数来考虑从任何类型到std::basic_string_view的隐式转换(例如从std::stringstd::string_view的隐式转换)。可以通过抑制其中一个参数的推导来实现这一点,示例部分在[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;

通过将其中一个操作数的类型定义为template <class T> using __identity = decay_t<T>;__identity,它引入了一个非推导上下文,为某些std::basic_string_view和另一个隐式转换为相同实例化的std::basic_string_view类模板的参数创建了一个重载。请注意保留HTML标签。

谢谢,我知道这可能超出了此问题的范围,但是decay_t<basic_string_view<charT, traits>>如何强制执行隐式转换呢? - golden retriever
@ goldenretriever std :: decay_t <T>是一个别名模板,它扩展为typename std :: decay <T> :: type。编译器看到::type是一个依赖类型(取决于T),不会尝试推断T。这是所谓的非推导上下文。但是,如果T也在另一个函数参数的定义中使用,并且可以在那里推断出来,则将用它来实例化处于非推导上下文中的该参数。最终,您将获得接受两个basic_string_viewoperator ==,但仅从一个参数的模板参数。 - Piotr Skotnicki
@ goldenretriever 将参数传递给函数遵循复制初始化语义,这只能使用隐式转换。 - Piotr Skotnicki

5
这是因为在[string.view.comparisons]中有一个奇怪的条款:

Sbasic_­string_­view<charT, traits>svS的实例。实现应提供足够的额外重载,标记为constexprnoexcept,以便具有隐式转换为S的对象t可以根据表62进行比较。

表62列出了所有比较运算符及其两侧的视图。

由于std::string可以隐式转换为std::string_view,因此将选择这种重载。这种重载将与s1 == s2情况完全匹配,因此不会考虑隐式转换。

基本上,这是通过SFINAE工具实现的。类似于这样:

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);

这种过载不需要隐式转换,因此比任何需要隐式转换的过载都更好。

嗯...这段话并没有说这样的重载应该生成一个精确匹配,而下面的“符合样例实现”也不是基于一个会这样做的函数模板。 - Piotr Skotnicki
@PiotrSkotnicki:“应提供足够的附加重载...以便具有对S的隐式转换的对象t可以进行比较”。如果它们不是精确匹配,则与string_view比较将产生歧义,因此它们无法进行比较。因此,精确匹配的要求是必需的。此外,几乎没有办法实现这样的事情而不使其成为精确匹配,即使忽略创建歧义的情况。 - Nicol Bolas
@PiotrSkotnicki:我不知道你的意思。示例代码是解释如何编写一个仅适用于可以隐式转换为“string_view”的类型的函数。它并不意味着是标准要求的“operator==” 的全部完整实现。 - Nicol Bolas
感谢Nicol和Piotr。我认为我理解了精确匹配将消除歧义的概念。但由于我在通常参考的网站https://en.cppreference.com中没有找到类似的实现,我想知道Piotr之前提到的因为字符串构造函数被标记为“explicit”,所以`operator==(string, string)`不会被选中,这也是原因吗?对我来说这也是有道理的。 - golden retriever
1
@goldenretriever,没有bool operator==(string, string)。相反,有template<class C, class T, class A> bool operator==(const basic_string<C, T, A>&, const basic_string<C, T, A>&),只有当两个操作数都可以推导成功时才有效,因此它不考虑转换,这就是为什么我的答案被删除的原因。 - Piotr Skotnicki
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接