Search code examples
c++c++11castingimplicit-conversionexplicit-conversion

Why C++ implicit conversion works, but explicit one does not?


The following code compiles successfully in C++11:

#include "json.hpp"
using json = nlohmann::json ;

using namespace std ;

int main(){
    json js = "asd" ;
    string s1 = js ; // <---- compiles fine
    //string s2 = (string)js ; // <---- does not compile
}

It includes JSON for Modern C++. A working example is in this wandbox.

The JSON variable js is implicitly converted to a string. However, if I uncomment the last line, which is an explicit conversion, it fails to compile. Compilation results here.

Beyond the particular nuances of this json library, how do you code a class so that an implicit conversion works but an explicit one does not?
Is there some kind of constructor qualifier that allows this behavior?


Solution

  • Here's a simplified code that reproduces the same issue:

    struct S
    {
        template <typename T>
        operator T() // non-explicit operator
        { return T{}; }
    };
    
    struct R
    {
        R() = default;
        R(const R&) = default;
        R(R&&) = default;
        R(int) {} // problematic!
    };
    
    int main()
    {
        S s{};
        R r = static_cast<R>(s); // error
    }
    

    We can see the compile error is similar:

    error: call of overloaded 'R(S&)' is ambiguous
         R r = static_cast<R>(s);
                               ^
    note: candidates...
         R(int) {}
         R(R&&) = default;
         R(const R&) = default;
    

    The problem relies on the generic S::operator T(), which will happily return a value to whatever type you want. For example, assigning s to any type will work:

    int i = s; // S::operator T() returns int{};
    std::string str = s; // S::operator T() returns std::string{};
    

    T is deduced to the conversion type. In the case of std::string, it has a lot of constructors, but if you do a copy-initialization(1) of the form object = other, T is deduced to the left-hand object's type (which is std::string).

    Casting is another matter. See, it's the same problem if you try to copy-initialize using the third form (which in this case is a direct initialization):

    R r(s); // same ambiguity error
    

    Okay, what are the constructor overloads for R again?

    R() = default;
    R(const R&) = default;
    R(R&&) = default;
    R(int) {}
    

    Given that R's constructors can take either another R, or int, the problem becomes apparent, as the template type deduction system doesn't know which one of these is the correct answer due to the context in which the operator is called from. Here, direct initialization has to consider all the possible overloads. Here's the basic rule:

    A is the type that is required as the result of the conversion. P is the return type of the conversion function template

    In this case:

    R r = s;
    

    R is the type that is required as the result of the conversion (A). However, can you tell which type A will represent in the following code?

    R r(s);
    

    Now the context has R and int as options, because there is a constructor in R that takes integers. But the conversion type needs to be deduced to only one of them. R is a valid candidate, as there is at least one constructor which takes an R. int is a valid candidate as well, as there is a constructor taking an integer too. There is no winner candidate, as both of them are equally valid, hence the ambiguity.

    When you cast your json object to an std::string, the situation is exact the same. There is a constructor that takes an string, and there is another one that takes an allocator. Both overloads are valid, so the compiler can't select one.

    The problem would go away if the conversion operator were marked as explicit. It means that you'd be able to do std::string str = static_cast<std::string>(json), however you lose the ability to implicitly convert it like std::string str = json.