What Is a Converting Constructor in C++ ? What Is It For

What is a converting constructor in C++ ? What is it for?

The definition for a converting constructor is different between C++03 and C++11. In both cases it must be a non-explicit constructor (otherwise it wouldn't be involved in implicit conversions), but for C++03 it must also be callable with a single argument. That is:

struct foo
{
foo(int x); // 1
foo(char* s, int x = 0); // 2
foo(float f, int x); // 3
explicit foo(char x); // 4
};

Constructors 1 and 2 are both converting constructors in C++03 and C++11. Constructor 3, which must take two arguments, is only a converting constructor in C++11. The last, constructor 4, is not a converting constructor because it is explicit.

  • C++03: §12.3.1

    A constructor declared without the function-specifier explicit that can be called with a single parameter specifies a conversion from the type of its first parameter to the type of its class. Such a constructor is called a converting constructor.

  • C++11: §12.3.1

    A constructor declared without the function-specifier explicit specifies a conversion from the types of its parameters to the type of its class. Such a constructor is called a converting constructor.

Why are constructors with more than a single parameter considered to be converting constructors in C++11? That is because the new standard provides us with some handy syntax for passing arguments and returning values using braced-init-lists. Consider the following example:

foo bar(foo f)
{
return {1.0f, 5};
}

The ability to specify the return value as a braced-init-list is considered to be a conversion. This uses the converting constructor for foo that takes a float and an int. In addition, we can call this function by doing bar({2.5f, 10}). This is also a conversion. Since they are conversions, it makes sense for the constructors they use to be converting constructors.

It is important to note, therefore, that making the constructor of foo which takes a float and an int have the explicit function specifier would stop the above code from compiling. The above new syntax can only be used if there is a converting constructor available to do the job.

  • C++11: §6.6.3:

    A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

    §8.5:

    The initialization that occurs [...] in argument passing [...] is called copy-initialization.

    §12.3.1:

    An explicit constructor constructs objects just like non-explicit constructors, but does so only where the direct-initialization syntax (8.5) or where casts (5.2.9, 5.4) are explicitly used.

What are the conversion constructors

A conversion constructor is any non-explicit constructor callable with one argument. In your code sample, as the Complex constructor provides default values for its parameters, it can be called with a single argument (say 3.0). And since such constructor is not marked explicit, then it is a valid conversion constructor.

When you do com1 == 3.0 --given that there is no operator == between Complex and double-- a conversion constructor is invoked. So your code is equivalent to this:

if( com1 == Complex(3.0) )

Converting constructor with multiple arguments

You might be especially surprised that:

x = {1, 2, 3};            // ok
x == {1, 2, 3}; // error
operator==(x, {1, 2, 3}); // ok

This is because there are just specific places where a braced-init-list (basically, a comma-delimited list of stuff between {}s) is allowed to go in the language. It can go on the right-hand side of = because the rules say it can. It can be used as an argument in a function call expression because the rules say it can. But it cannot be used on either side of the comparison operators because the rules don't allow for it.

I do not know if there is a fundamental reason behind this beyond there probably not being a strong need for it.

What changed with converting constructors at C++11?

An example with two parameters is given later in the page you cited:

A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)

How to correctly use converting constructors?

You also need to understand that you are using copy initialization rather than direct initialization.

They are different, and you need to understand how, and their relationship with explicit. You need to understand how chains of conversions work, with at most one user-defined conversion involved.

Dog d1 ("rover");
Dog d2 = "rover";

The d2 case tries to convert the literal to a Dog, and then copy (move) that to d2. But that would be a double conversion: const char* to string and then string to Dog.

The d1 case constructs d1 passing the argument to the constructor, so only one conversion const char* to string. (In both cases, promoting const char [6] to const char* is in there too but doesn't count toward the "only one" allowed, being in a different category.)

The copy-initialization does not specify "rover" as an argument of the constructor. It says "here is something, but a Dog is needed here". here is the right-hand-side of the copy-init declaration syntax, not any identifiable function. The compiler than has to find a legal conversion.

In the direct-init case, you are simply giving parameters for a function (a constructor). The compiler converts what you gave it into the declared argument type.

Conversion Constructor in C++

How to fix this without editing any header file.

You can't. The definition of Polar(Rectangle) will have to come after the definition of Rectangle, so that Rectangle is complete where the constructor needs to use it.

Just declare the constructor in the class definition:

Polar(Rectangle r);

and define it elsewhere; either in a source file, or in a header after defining Rectangle (in which case you'll need to mark it inline).

Personally, I'd tidy this up by splitting it into two headers, one per class, and defining all the members in source files (unless I've proved that they need to be inline for performance reasons). Then the headers will each only need to declare the other class, and only need to be included from the source files that implement or use the classes.

Using converting constructor in multiple parameter case

Implicit conversion won't be considered in template argument deduction. Given 3*r3, template argument deduction for T based on the 1st function argument (i.e. 3) fails; the implicit conversion from int to rational<int> won't be considered.

Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.

You can change operator* to non-template, which doesn't have such issue and implicit conversion would work as you expected.

template <typename> class rational;
template <typename T> std::ostream& operator<<(std::ostream&, const rational<T> &);

template <typename T>
class rational {
friend std::ostream& operator<<<T>(std::ostream&, const rational<T>&);
friend rational<T> operator*(const rational<T> &lo, const rational<T> &ro) {
rational<T> result(ro);
result *= lo;
return result;
}
public:
rational();
rational(T num);
rational(T num, T den);
rational(const rational &ro);

rational &operator+=(const rational &ro);
rational &operator*=(const rational &ro);
...
...
private:
T num, den;
};

Conversion constructor vs. conversion operator: precedence

You do copy initialization, and the candidate functions that are considered to do the conversions in the conversion sequence are conversion functions and converting constructors. These are in your case

B(const A&)
operator B()

Now, that are the way you declare them. Overload resolution abstracts away from that, and transforms each candidate into a list of parameters that correspond to the arguments of the call. The parameters are

B(const A&)
B(A&)

The second one is because the conversion function is a member function. The A& is the so-called implicit object parameter that's generated when a candidate is a member function. Now, the argument has type A. When binding the implicit object parameter, a non-const reference can bind to an rvalue. So, another rule says that when you have two viable functions whose parameters are references, then the candidate having the fewest const qualification will win. That's why your conversion function wins. Try making operator B a const member function. You will notice an ambiguity.

From an object-oriented philosophical standpoint, is this the way the code should behave? Who knows more about how an A object should become a B object, A or B? According to C++, the answer is A -- is there anything in object-oriented practice that suggests this should be the case? To me personally, it would make sense either way, so I'm interested to know how the choice was made.

For the record, if you make the conversion function a const member function, then GCC will chose the constructor (so GCC seems to think that B has more business with it?). Switch to pedantic mode (-pedantic) to make it cause a diagnostic.


Standardese, 8.5/14

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3).

And 13.3.1.4

Overload resolution is used to select the user-defined conversion to be invoked. Assuming that "cv1 T" is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:

  • The converting constructors (12.3.1) of T are candidate functions.
  • When the type of the initializer expression is a class type "cv S", the conversion functions of S and its base classes are considered. Those that are not hidden within S and yield a type whose cv-unqualified version is the same type as T or is a derived class thereof are candidate functions. Conversion functions that return "reference to X" return lvalues of type X and are therefore considered to yield X for this process of selecting candidate functions.

In both cases, the argument list has one argument, which is the initializer expression. [Note: this argument will be compared against the first parameter of the constructors and against the implicit object parameter of the conversion functions. ]

And 13.3.3.2/3

  • Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if [...] S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.


Related Topics



Leave a reply



Submit