C++ implicit conversions with brace initializers
Asked Answered
F

2

11

I've recently read somewhere (can't remember where) about using braces to allow multiple user-defined conversions, but there seems to be a difference between conversion by constructor and conversion by conversion method that I don't understand.

Consider:

#include <string>

using ::std::string;

struct C {
  C() {}
};

struct A {
  A(const string& s) {}  // Make std::string convertible to A.
  operator C() const { return C(); }  // Makes A convertible to C.
};

struct B {
  B() {}
  B(const A& a) {}  // Makes A convertible to B.
};

int main() {
  B b;
  C c;

  // This works.
  // Conversion chain (all thru ctors): char* -> string -> A -> B
  b = {{"char *"}};

  // These two attempts to make the final conversion through A's
  // conversion method yield compiler errors.
  c = {{"char *"}};
  c = {{{"char *"}}};
  // On the other hand, this does work (not surprisingly).
  c = A{"char *"};
}

Now, I may be misinterpreting what the compiler is doing, but (based on the above and additional experimentation) it seems to me that it's not considering conversions by conversion-method. Reading through Sections 4 and 13.3.3.1 of the standard, however, I wasn't able to find a clue why this is. What is the explanation?

Update

Here's another interesting phenomenon I'd like explained. If I add

struct D {
  void operator<<(const B& b) {}
};

and in main:

  D d;
  d << {{ "char *" }};

I get an error, but if instead I write d.operator<<({{ "char *" }}); it works fine.

Update 2

Looks like Section 8.5.4 in the standard may hold some answers. I'll report my findings.

Fireproof answered 27/5, 2016 at 11:40 Comment(5)
An initialisation uses constructors, and will not use an intermediary type's conversion operator. The two non-working examples fail because implicitly constructing an A in order to use its operator C goes against this.Hildebrandt
Related: #35791164Rattan
Peter, what I'm trying to understand is what the rules are exactly. If I write c = A{... or c = {A{... it works fine through the conversion method. Why does it decide to only use ctors if I drop the A?Fireproof
I hope you are aware that having anything of the sort in any code which won't be abandoned in a month would be a rather bad idea. It's always better to know what the compiler is doing, rather than letting it go wild.Straiten
Um what. Why do you expect C to implicitly convert to an unrelated class, A?Thrippence
Z
7

There is one user conversion possible.

In b = {{"char *"}};

we actually do

b = B{{"char*"}}; // B has constructor with A (and a copy constructor not viable here)

so

b = B{A{"char*"}}; // One implicit conversion const char* -> std::string

in c = {{"const char*"}}, we try

c = C{{"char *"}}; // but nothing to construct here.
Zeta answered 27/5, 2016 at 11:59 Comment(4)
@Ari: it will do c = C{A{std::string{"char*"}}}. there is justA->C conversion.Zeta
But that's the question, isn't it? Why doesn't this happen when A is dropped? I thought you were saying that C{{ doesn't work because C doesn't have a ctor that takes A as an argument. Clearly, C{A or just {A isn't constructing, but rather converting through A's method. Why doesn't this happen without saying A?Fireproof
There is no C constructor which takes A, so {{"char*"}} cannot be deduced to type A.Zeta
@Zeta Ari is asking why it is limited to ctor when there is a conversion operator available. Your answer just re-iterates what he states in his question.Distinguish
F
1

Digging through Section 8.5.4 of the standard and following various cross references therein, I think I understand what's going on. Of course, IANAL, so I might be wrong; this is my best effort.

Update: The previous version of the answer actually used multiple conversions. I've updated it to reflect my current understanding.

The key to unraveling the mess is the fact that a braced-init-list is not an expression (which also explains why d << {{"char *"}} won't compile). What it is is special syntax, governed by special rules, that is allowed in a number of specific contexts. Of these contexts, the relevant ones for our discussion are: rhs of assignment, argument in a function call, and argument in a constructor invocation.

So what happens when the compiler sees b = {{"char *"}}? This is a case of rhs of assignment. The applicable rule is:

A braced-init-list may appear on the right-hand side of ... an assignment defined by a user-defined assignment operator, in which case the initializer list is passed as the argument to the operator function.

(Presumably, the default copy assignment operator is considered a user-defined assignment operator. I couldn't find a definition of that term anywhere, and there doesn't seem to be any language allowing the brace syntax specifically for default copy assignment.)

So we are reduced to argument passing to the default copy assignment operator B::operator=(const B&), where the argument being passed is {{"char *"}}. Because a braced-init-list is not an expression, there is no issue of conversion here, but rather a form of initialization of a temporary of type B, specifically, so called list initialization.

If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

So the compiler strips off the outer pair of braces and performs overload resolution using {"char *"} as the argument. This succeeds, matching the constructor B::B(const A&) because there is again list initialization of a temporary of type A in which overload resolution succeeds to match A::A(const string&) for the argument "char *", which is possible through the one alloted user-defined conversion, namely, from char* to string.

Now, in the case of c = {{"char *"}} the process is similar, but when we try to list-initialize a temporary of type C with {{"char *"}}, overload resolution fails to find a constructor that matches. The point is that by definition list-initialization only works through a constructor whose parameter list can be made to match the contents of the list.

Fireproof answered 28/5, 2016 at 18:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.