使用自定义转换运算符的不明确性

Clang ambiguity with custom conversion operator

本文关键字:不明确 明确性 运算符 转换 自定义      更新时间:2023-10-16

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

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&函数修饰符的理解正确吗?

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

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

同一形式的两个隐式转换序列是不可区分的转换序列,除非其中一个 以下规则适用:

  • [...]

  • 用户定义的转换序列 U1 是比其他用户定义的转换更好的转换序列 序列 U2 如果它们包含相同的用户定义的转换函数或构造函数 [...]

由于右值始终可以绑定到常量左值引用,因此如果函数重载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的错误。

但如果你必须使用Clang,托尼·弗罗洛夫的答案就是解决方案。

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 lvalue 对象中可行,但不能与右值一起使用。我通过将 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 );
}

根据[dcl.init.ref]/5,用于通过绑定到 右值,引用必须是常量非易失性左值 引用或右值引用:

而左值引用和常量左值引用可以绑定到常量易失引用。显然,成员函数上的易失性修饰符服务于完全不同的事情。但是,嘿,它适用于我的用例并且足够了。唯一剩下的问题是代码变得具有误导性和惊人性。

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

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