6

I'm getting a compiler error about a type conversion that should be legal, to my eyes.

Let's say I have a home-grown string class, convertible from and to char*:

class custom_string { public: custom_string(const char* str) : _str{str} {} operator const char*() const { return _str; } private: const char* _str; }; 

Out in the wild, I want to use this custom class interchangeably with std::string in a number of ways:

struct pod_t { std::string str; int other_data; }; void test() { custom_string cstr("Hello"); std::set<std::string> strings; strings.emplace(cstr); pod_t pod {cstr, 42}; // C2440: 'initializing': cannot convert from 'custom_string' to 'std::string' } 

I'm using MSVC with the /std:c++20 flag.

Why does the last line result in a compiler error? If the compiler can figure out the path from custom_string to std::string in the case of the emplace function (presumably it's using the char* operator), why can't it do the same when I'm trying to initialize the struct?

9
  • 2
    The compiler will only call one constructor or method in automatic typecasting. Your code requires two: one from custom_string to char *, and another from char * to std::string. You need to add an operator std::string to your custom string, which pretty much defeats the purpose. Commented Aug 14 at 23:42
  • Thanks @user207421. But std::string can be constructed from a custom_string, i.e. this compiles: std::string test(cstr); The fact that it can be directly constructed is not enough to "assist" the compiler in the pod_t case? Commented Aug 15 at 0:06
  • std::string test(cstr); is one conversion. pod_t pod {cstr, 42}; is two conversions. You can do pod_t pod {std::string{cstr}, 42}; to do one conversion. Commented Aug 15 at 0:07
  • @PeterYurkosky std::string test(cstr) is not a conversion. It is direct-initialization which does overload resolution against constructors of std::string and selects one. The implicit conversion sequence is then in the argument of the selected constructor. In this conversion sequence there can also only be one user-defined conversion (i.e. construct/conversion function call). Commented Aug 15 at 0:34
  • 1
    So I imagine the emplace call can also use direct-initialization, because of the perfect forwarding of the argument types? And direct-initialization takes advantage of constructor overloading on the "target", while implicit conversion can only take advantage of user-defined conversion operators on the source? @user17732522 do I have that right? Commented Aug 15 at 0:51

1 Answer 1

7

In direct initialization you are allowed 1 implicit conversion before the overload resolution of the constructors, as if you are calling a function that takes the same arguments as the constructor.

struct Converted {}; struct Base { operator Converted() const { return {}; } }; struct Final { Final(Converted) {} }; void func_converted(Converted) {} Base base; func_converted(base); // compiles Final f{base}; // first converts to Converted then overload resolution of constructors std::vector<Final> finals; finals.emplace_back(base); // compiles because it does Final{base} internally 

In overload resolution only 1 implicit conversion is allowed between the input type and the argument type, a function taking Final will have to do Base -> Converted -> Final which is 2 implicit conversions, not 1.

struct FinalHolder { Final f; }; void func_final(Final) {} Base base; FinalHolder h{base}; // error: could not convert 'base' from 'Base' to 'Final' func_final(base); // error: could not convert 'base' from 'Base' to 'Final' std::vector<Final> finals; finals.push_back(base); // doesn't compile because it is same as above 

demo to illustrate

Aggregate initialization is similar to calling a function and follows the same rules, you can trigger direct initialization manually allowing an extra implicit conversion before overload resolution is done.

func_final({base}); // compiles FinalHolder h{{base}}; // compiles 
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks @Ahmed AEK. So picking apart FinalHolder h{{base}}: is it that the inner set of braces forces a 2nd direct initialization, and so with two chained initializations, is able to find its way from Base to Converted to Final?
yes, the second pair of braces make the compiler look for a constructor reachable after 2 implicit conversions done on base instead of just 1. it finds a constructor taking in Final, it checks whether Final can be constructed from Base after 2 implicit conversions and it is possible, so the compilers picks this overload, and does the 2 implicit conversions.
And -- just so I understand -- adding a third pair of braces (FinalHolder h{{{base}}}) would permit a 3-conversion path (just theoretically, if one were needed) to Final?
because it needs to do overload resolution to figure this out means that variadic functions like make_unique won't work, because variadic functions can't do overload resolution, so you must do unique_ptr<FinalHolder>{ new FinalHolder{{base}}};
yes, it will work, but conversion operators are only considered once, the rest will have to come through converting constructors. although MSVC is warning that it is doing multiple implicit conversions, but it is totally legal godbolt.org/z/x5dcc8sK7 , if you want to get rid of the warning then just make the conversion explicit.
Quite an education. Thanks a lot for all the attention to this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.