Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clang ambiguity with custom conversion operator

I've been developing a kind of adapter class, when I encountered a problem under clang. When both conversion operators for lvalue-reference and rvalue reference are defined you get an ambiguity compilation error trying to move from your class (when such code should be fine, as

operator const T& () const&

is allowed only for lvalues AFAIK). I've reproduced error with simple example:

#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"
}

Compiles fine on gcc 4.9+, fails on any clang version. So the question: is there any workaround? Is my understanding of const& function modifier right?

P.S.: To clarify - the question is about fixing StringDecorator class itself (or finding the workaround for such class as if were a library code). Please refrain providing answers that call operator T&&() directly or specify conversion type explicitly.

like image 709
Tony Frolov Avatar asked Dec 05 '17 16:12

Tony Frolov


3 Answers

The problem comes from the selection of the best viable function. In the case of the second func call, it implies the comparison of 2 user-defined conversion sequence. Unfortunately, 2 user-defined conversion sequence are undistinguishable if they do not use the same user-defined conversion function or constructor 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 [...]

Because a rvalue can always bind to a const lvalue reference, you will in any case fall on this ambiguous call if a function is overloaded for const std::string& and std::string&&.

As you mentioned it, my first answer consisting in redeclaring all functions is not a solution as you are implementing a library. Indeed it is not possible to define proxy-functions for all functions taking a string as argument!!

So that let you with a trade off between 2 imperfect solutions:

  1. You remove operator std::string&&() &&, and you will loose some optimization, or;

  2. You publicly inherit from std::string, and remove the 2 conversion functions, in which case you expose your library to misuses:

    #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.
    }
    

An other solution is not to use Clang because it is a bug of Clang.

But if you have to use Clang, the Tony Frolov answer is the solution.

like image 156
Oliv Avatar answered Nov 02 '22 19:11

Oliv


Oliv answer is correct, as standard seems to be quite clear in this case. The solution I've chosen at a time was to leave only one conversion operator:

operator const std::string& () const&

The problem exists because both conversion operators are considered viable. So this can be avoided by changing type of implicit argument of lvalue conversion operator from const& to &:

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

But this breaks conversion from const StringDecorator, making its usage awkward in typical cases.

This broken solution led me thinking if there is a way to specify member function qualifier that will make conversion operator viable with const lvalue object, but not with the rvalue. And I've managed to achieve this by specifying implicit argument for const conversion operator as 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:

While lvalue reference and const lvalue reference can bind to const volatile reference. Obviously volatile modifier on member function serves completely different thing. But hey, it works and sufficient for my use-case. The only remaining problem is that code becomes misleading and astonishing.

like image 24
Tony Frolov Avatar answered Nov 02 '22 19:11

Tony Frolov


clang++ is more accurate. Both func overloads are not exact matches for StringDecorator const& or StringDecorator&&. Thus my_string can not be moved. The compiler can not choose between possible transforms StringDecorator&& --> std::string&& --> func(std::string&&) and StringDecorator&& -> StringDecorator& --> std::string& --> func(const std::string&). In other words the compiler can not determine on what step it should apply cast operator.

I do not have g++ installed to check my assumption. I guess it goes to the second way, since my_string can not be moved, it applies the cast operator const& to StringDecorator&. You can check it if you add debug output into bodies of cast operators.

like image 1
273K Avatar answered Nov 02 '22 18:11

273K