自定义转换运算符的 Clang 歧义

Clang ambiguity with custom conversion operator

我一直在开发一种适配器class,当我在clang下遇到问题。当定义了左值引用和右值引用的转换运算符时,您会在尝试从 class 移动时遇到歧义编译错误(当这样的代码应该没问题时,如

operator const T& () const&

仅允许左值 AFAIK)。我用简单的例子重现了错误:

#include <string>

class StringDecorator
{
public:
  StringDecorator()
  : m_string( "String data here" )
  {}

  operator const std::string& () const& // lvalue only
  {
    return m_string;
  }

  operator std::string&& () && // rvalue only
  {
    return std::move( m_string );
  }

private:
    std::string m_string;
};

void func( const std::string& ) {}
void func( std::string&& ) {}

int main(int argc, char** argv)
{
  StringDecorator my_string;

  func( my_string ); // fine, operator std::string&& not allowed
  func( std::move( my_string ) ); // error "ambiguous function call"
}

在 gcc 4.9+ 上编译良好,在任何 clang 版本上编译失败。 所以问题是:有什么解决方法吗?我对 const& 函数修饰符的理解对吗?

P.S.: 澄清一下 - 问题是关于修复 StringDecorator class 本身(或者为这样的 class 找到解决方法,就好像是库代码一样)。请不要提供直接调用运算符 T&&() 或明确指定转换类型的答案。

问题来自最佳可行函数的选择。在第二次 func 调用的情况下,它意味着比较 2 用户定义的转换序列 。不幸的是,2 用户定义的转换序列如果不使用相同的用户定义的转换函数或构造函数C++ standard 无法区分 [ over.ics.rank/3]:

Two implicit conversion sequences of the same form are indistinguishable conversion sequences unless one of the following rules applies:

  • [...]

  • User-defined conversion sequence U1 is a better conversion sequence than another user-defined conversion sequence U2 if they contain the same user-defined conversion function or constructor [...]

因为右值总是可以绑定到 const 左值引用,所以如果函数为 const std::string&std::string&&.

重载,您无论如何都会陷入这种模棱两可的调用

正如您所提到的,我的第一个答案是重新声明所有函数,这不是解决方案,因为您正在实现一个库。事实上,不可能为所有以 string 作为参数的函数定义代理函数!

这样您就可以在 2 个不完美的解决方案之间进行权衡:

  1. 您删除 operator std::string&&() &&,您将失去一些优化,或者;

  2. 您公开继承自 std::string,并删除了 2 个转换函数,在这种情况下,您的库会被滥用:

    #include <string>
    
    class StringDecorator
      : public std::string
    {
    public:
      StringDecorator()
      : std::string("String data here" )
      {}
    };
    
    void func( const std::string& ) {}
    void func( std::string&& ) {}
    
    int main(int argc, char** argv)
    {
      StringDecorator my_string;
    
      func( my_string ); // fine, operator std::string&& not allowed
      func( std::move( my_string  )); // No more bug:
        //ranking of standard conversion sequence is fine-grained.
    }
    

另一个解决方案是不使用 Clang,因为它是

但如果您必须使用 Clang,Tony Frolov 的回答就是解决方案。

clang++ 更准确。两个 func 重载都不完全匹配 StringDecorator const&StringDecorator&&。因此 my_string 无法移动。编译器无法在可能的转换 StringDecorator&& --> std::string&& --> func(std::string&&)StringDecorator&& -> StringDecorator& --> std::string& 之间进行选择--> func(const std::string&)。换句话说,编译器无法确定应该在哪一步应用强制转换运算符。

我没有安装 g++ 来检查我的假设。我想它是第二种方式,因为 my_string 不能移动,它将转换运算符 const& 应用于 StringDecorator&。如果将调试输出添加到转换运算符的主体中,则可以检查它。

Oliv 的回答是正确的,因为在这种情况下标准似乎很清楚。我曾经选择的解决方案是只保留一个转换运算符:

operator const std::string& () const&

问题存在是因为两个转换运算符都被认为是可行的。因此,这可以通过将左值转换运算符的隐式参数类型从 const& 更改为 &:

来避免
operator const std::string& () & // lvalue only (rvalue can't bind to non-const reference)
{
    return m_string;
}

operator std::string&& () && // rvalue only
{
    return std::move( m_string );
}

但这会中断从 const StringDecorator 的转换,使其在典型情况下的使用很尴尬。

这个失败的解决方案让我想到是否有一种方法可以指定成员函数限定符,使转换运算符对 const 左值对象可行,但对右值对象不可行。我已经设法通过将 const 转换运算符的隐式参数指定为 const volatile&:

来实现这一点
operator const std::string& () const volatile& // lvalue only (rvalue can't bind to volatile reference)
{
    return const_cast< const StringDecorator* >( this )->m_string;
}

operator std::string&& () && // rvalue only
{
    return std::move( m_string );
}

Per [dcl.init.ref]/5, for a reference to be initialized by binding to an rvalue, the reference must be a const non-volatile lvalue reference, or an rvalue reference:

虽然左值引用和 const 左值引用可以绑定到 const volatile 引用。显然,成员函数上的 volatile 修饰符提供完全不同的东西。但是,嘿,它对我的​​用例来说已经足够了。唯一剩下的问题是代码变得误导和令人惊讶。