c++constructorc++20implicit-conversionaggregate-initialization

Why can my C++ class be implicitly converted in one case, but not another?


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?


Solution

  • 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