Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 254
GetFree Avatar asked Apr 15 '17 01:04

GetFree


People also ask

Does C allow implicit conversion?

Implicit type conversion in C language is the conversion of one data type into another datatype by the compiler during the execution of the program. It is also called automatic type conversion.

Why is implicit type conversion bad?

Implicit conversions allow the compiler to treat values of a type as values of another type. There's at least one set of scenarios in which this is unambiguously bad: non-total conversions. That is, converting an A to a B when there exists A s for which this conversion is impossible.

What is implicit conversion not possible?

We cannot perform implicit type casting on the data types which are not compatible with each other such as: Converting float to an int will truncate the fraction part hence losing the meaning of the value. Converting double to float will round up the digits.

Can there be loss of data as a result of an implicit conversion?

"It is possible for implicit conversions to lose information, signs can be lost (when signed is implicitly converted to unsigned), and overflow can occur (when long long is implicitly converted to float)."


2 Answers

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.

like image 139
Mário Feroldi Avatar answered Nov 08 '22 01:11

Mário Feroldi


I think it is that when you use explicit conversion, the compiler have to choose from more function that when the code use implicit conversion. When compiler found

string s1 = js 

it exclude from overloading, all costructor and conversion marked "explicit" so it results to pick one function. Instead when compiler found :

string s2 = (string)js ;

it must include all conversion and then the ambiguity.

like image 35
alangab Avatar answered Nov 08 '22 01:11

alangab