Search code examples
c++stringoperator-overloading

How to resolve ambiguous overload for 'operator=' in string


I am trying to create a class that can be implicity cast to a variety of different types, both primitives and custom defined classes. One of the types that I want to be able to cast to is an std::string. Below is an example class that can cast to various different types. It throws the error "error: ambiguous overload for ‘operator=’". This is because std::string has an assignment operator from a CharT which the compiler can create from an int. My question is, is it possible to have a class that can both implicity convert to an integer or a string type?

class Test {
public:
    operator double() const {
        return 3.141592;
    }
    operator std::int64_t() const {
        return -999;
    }
    operator std::uint64_t() const {
        return 999;
    }
    operator bool() const {
        return true;
    }
    operator std::string() const {
        return "abcd";
    }
};


int main(int argc, char** argv) {
    std::string test_str = Test();
    test_str = Test();
    std::cout << test_str;
}

Interestingly, when I assign to test_str on the same line that I define it, the compiler throws no errors because it's using the simple constructor rather than an assignment operator, but it errors on the following line.

Any help would be much appreciated.


Solution

  • std::string has several overloaded operator=s with following parameters: std::string, const char *, char, std::initializer_list.

    For your code to work, the compiler needs to choose one, but at least two are potentially suitable: the std::string one; and the char one, using an implicit conversion from one of your scalar operators. Even though the latter would cause an ambiguity later on, the compiler doesn't seem to reach that point.

    The solution is to make a single templated conversion operator, and to restrict it to specific types with SFINAE:

    Then following works:

    template <auto V, typename, typename...>
    inline constexpr auto value = V;
    
    template <typename T, typename ...P>
    concept one_of = (std::same_as<T, P> || ...);
    
    class Test
    {
      public:
        template <one_of<int, float, std::string> T>
        operator T() const
        {
            if constexpr (std::same_as<T, int>)
                return 42;
            else if constexpr (std::same_as<T, float>)
                return 42;
            else if constexpr (std::same_as<T, std::string>)
                return "foo";
            else
                static_assert(value<false, T>, "This shouldn't happen.");
        }
    };
    

    The static_assert isn't really necessary, since we already have requires. It merely provides a nicer error if you add more types to the requires and forget a branch.

    Note value<false, T> instead of false. Trying to put false there directly would cause some compilers to emit an error unconditionally, even if the branch is not taken (this is allowed, but not required). Using an expression dependendent on T convinces the compiler to delay the test until an actual instantiation (because for all it knows, value could be specialized to become true for some T).


    when I assign to test_str on the same line that I define it

    This is an initialization, not assignment. It calls a constructor, not operator=.