Constructors and conversions

150 views Asked by At

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 ints 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
}
5

There are 5 answers

1
Steve Jessop On BEST ANSWER

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.

2
Dietmar Kühl On

Clearly, a Foo can be initialized from two integers:

Foo f = { 1, 2 };

It takes one construction to produce the first argument of Bar of type Foo from an { 1, 2 }, i.e., Foo{ 1, 2 }. It takes another construction to produce the second argument of Bar of type Foo from { 3, 4 }. Actually, there isn't any implicit conversion involved.

That said, please note that the number of conversions needed for different subexpressions don't count against the one conversion: if you have multiple subexpressions, each one involving one conversion to satisfy an argument, that's OK.

0
juanchopanza On

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 ints should not translate into a call to Bar’s ctor with two Foo’s. Then, why does the following compile?

I am not sure how you got that implication, but it is the wrong way around: if Foo's two int constructor were declared explicit, then the code would not compile, because the conversion from initializer list to Foo would not work:

 Foo f = {1,2}; // error if Foo(int, int) is explicit

Without the explicit, this conversion is allowed, and there is only one user-defined conversion taking place in the construction of b, that is, from initializer list to Foo.

7
Johan On

This line is a bit tricky:

Bar b = { { 1, 2 }, { 3, 4 } };

This is a statement that will try to build a Bar. It take the constructor of Bar:

Bar(Foo first_, Foo second_);

Ok, so it does need to build 2 Foo, no problem, let's see what the constructor of Foois :

Foo(int x_, int y_);

Damn right ! We just got two { int, int } ! As remarked by @juanchopanza, this is where the conversion occurs. I did not think of braced initializer list to (int, int) as conversion but as you can see if you put explicit on your constructor it is one:

converting to ‘Foo’ from initializer list would use explicit constructor ‘Foo::Foo(int, int)’
   Bar b = { { 1, 2 }, { 3, 4 } };

Byt the way as pointed out in the comment, if you explicitly used std::initializer_list it does not compile: http://ideone.com/5enNFU

So it does build two Foo with those 2 initializers converted using your constructor, it does pass it to the Bar constructor, converting the initializer list containing two Foo :

Bar(Foo first_, Foo second_) : first(first_), second(second_) {}

And this constructor just call 2 copy constructor of Foo because first and second are of type Foo.

1
AnT stands with Russia On

The article you linked is written in terms of pre-C++11 version of the language, which completely ignores the new C++11 features related to uniform initialization. In older versions of the language user-defined conversions could be used implicitly to convert from a single object only.

In C++11 it became possible to use multi-parameter constructors to construct (and implicitly convert) objects from initializer of the { ... } form, where each consecutive element from between the {} is passed as the corresponding constructor argument. This form of implicit conversion can also be disabled by explicit keyword.

This C++11 conversion is exactly what happens in your sample code. First, the { 1, 2 } initializer is converted to Foo type using this new C++11 feature. Then the { Foo(1,2), Foo(3, 4) } is converted to Bar type through the same C++11 feature.

The code you posted will not be accepted by a pre-C++11 compiler. In fact, pre-C++11 compilers prohibit using { ... } initializers with objects that have user defined constructors, except for the situations when there's only one value inside the {}.