C++
I’ve read that constructors without the explicit
keyword and with one parameter (or a one-argument call to a ctor with several parameters, where all but one have default values) can perform one implicit conversion. Given two classes where Foo
’s ctor has two int
parameters and Bar
’s ctor has two Foo
parameters, that statement seems to imply a call to Bar
’s ctor with two unqualified sets of int
s should not translate into a call to Bar
’s ctor with two Foo
’s. Then, why does the following compile?
struct Foo {
Foo(int x_, int y_) : x(x_), y(y_) {}
int x;
int y;
};
struct Bar {
Bar(Foo first_, Foo second_) : first(first_), second(second_) {}
Foo first;
Foo second;
};
#include <iostream>
int main() {
Bar b = { { 1, 2 }, { 3, 4 } };
std::cout << b.first.x << ", " << b.first.y << ", ";
std::cout << b.second.x << ", " << b.second.y << std::endl;
// ^ Output: 1, 2, 3, 4
}
The statement implies nothing of the sort. It is a statement about constructors with one parameter, and you have written constructors with two parameters. It doesn't apply.
Any conversion sequence can contain at most one user-defined conversion (which, depending on the context of the conversion, may or may not be allowed to be explicit
-- in the case you describe it may not). Here there are two separate conversion sequences: one to get from the initializer list to the first argument of Bar
's constructor, and another to get from the other initializer list to the second argument. Each sequence contains one conversion, to Foo
. That's why your code is fine.
What you can't do under the "one user-defined conversion" rule is this:
struct Foo { Foo(int) {} };
struct Bar { Bar(const Foo&) {} };
struct Baz { Baz(const Bar&) {} };
Baz baz(1);
because that would require two user-defined conversions in the chain. But Baz baz(Foo(1));
is fine since only one conversion is needed to get from the argument you provided (an instance of Foo
) to an argument Baz
can accept (an instance of Bar
). Baz baz(Bar(1L))
is also fine, because although there are two conversions long -> int -> Foo
in order to get from 1L
to an argument that Bar()
can accept, one of the conversions is builtin, not user-defined.
You also can't do this:
Bar bar = 1;
because that's copy-initialization and it first tries to convert the right-hand-side to Bar
. Two user-defined conversions are needed in the chain int -> Foo -> Bar
, so it's no good. But you can do this:
Bar bar = { 1 };
It's equivalent in this case to Bar bar(1);
, not Bar bar = 1;
. There is a difference in general between T t = { 1 };
and T t(1)
, which is that with T t = { 1 };
the T
constructor cannot be explicit. But it doesn't count as part of the conversion chain for the initializer.