Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do these two constructors together not produce an ambiguity error?

Consider the following:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
};


int main()
{
    A a(1);
    return 0;
}

I have two constructors, one is a default and the other one is converting constructor with a default argument. When I try to compile the code, I expected an ambiguity error, but the compiler doesn't produce one.

Even when I don't create an instance of A, it doesn't produce an ambiguity error either.

int main()
{
    return 0;
}

Why is that?

like image 999
Ron_s Avatar asked Sep 01 '11 06:09

Ron_s


People also ask

What is constructor ambiguity?

This means that you have two constructors with the same signature, or that you're trying to create a new instance of Case with parameters that could match more than one constructor.

Can two constructors have the same parameters?

The technique of having two (or more) constructors in a class is known as constructor overloading. A class can have multiple constructors that differ in the number and/or type of their parameters. It's not, however, possible to have two constructors with the exact same parameters.

Can you have two constructors in C++?

In c++, there are multiple constructors in a class under the same name but a different list of arguments. This concept of constructor overloading in c++ is quite similar to function overloading. Usually, you should create more than one constructor in a class to initialize member variables differently for objects.

Can you have two constructors?

A class can have multiple constructors that assign the fields in different ways. Sometimes it's beneficial to specify every aspect of an object's data by assigning parameters to the fields, but other times it might be appropriate to define only one or a few.


2 Answers

There is no compilation error because no error exists in your code. Just like defining two functions: they need to have different signatures, other than that, it's possible. The ambiguity compilation error appears only when you try to call one of those functions. Same will happen trying to call the default constructor of A, even if it is private:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
    void f() {}
    void f(int x = 0) {}
};

This compiles, trying to call f() with no parameters fails, which makes sense.

Also try:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
    void f() {}
    void f() const {}
};

Should this give an error? No, because the two f's have different signatures. In this case the compiler can resolve the ambiguity, if you call f on a const object, the const method will get called, and vice-versa.

like image 174
Luchian Grigore Avatar answered Sep 28 '22 13:09

Luchian Grigore


Your code compiles because there is no ambiguity. You created a class with two constructors, one which always takes 0 arguments, and one which always takes one argument, an int. You then unambiguously called the constructor taking an int value. That this int has a default value does not matter, it is still a completely different signature. That the constructors are potentially ambiguous doesn't matter, the compiler only complains when a particular call is actually ambiguous.

When you create an instance of A with no arguments, it doesn't know which constructor you want to call: the default constructor, or the constructor taking an int with a parameter value of 0. In this case it would be nice if C++ notices that the private constructor is ineligible, but that is not always possible.

This behavior ends up being useful in some circumstances (e.g. if you have a few overloads involving templates, some of which will overlap if given the right types), though for simple cases like this, I would just make the single constructor with the default argument (preferably marked explicit unless you have a really really good reason to leave it implicit, and then I would second guess that reason just to be sure!)

-- EDIT --

Let's have some fun with this and try to explore further what is happening.

// A.h
class A
{
public:
    A(); // declare that somewhere out there, there exists a constructor that takes no args.  Note that actually figuring out where this constructor is defined is the linker's job
    A(int x = 10); // declare that somewhere out there, there exists a constructor that take one arg, an integer. Figuring out where it is defined is still the linker's job. (this line does _not_ declare two constructors.)

    int x;
};

// A.cpp
#include "A.h"

A::A() { ... } // OK, provide a definition for A::A()
A::A(int x) { ... } // OK, provide a definition for A::A(int) -- but wait, where is our default param??

// Foo.cpp
#include "A.h"

void Foo()
{
    A a1(24); // perfectly fine
    A a2; // Ambigious!
}

// Bar.cpp
class A // What's going on? We are redefining A?!?!
{
public:
    A();
    A(int x); // this definition doesn't have a default param!
    int x;
};

void Bar()
{
    A a; // This works! The default constructor is called!
}

// Baz.cpp
class A // What's going on? We are redefining A again?!?!
{
public:
    //A(); // no default constructor!
    A(int x = 42); // this definition has a default param again, but wait, it's different!
    int x;
};

void Baz()
{
    A a; // This works! A::A(int) is call! (but with what parameter?)
}

Note that we are taking advantage of the fact that the compiler doesn't know about headers; by the time it looks at a .cpp file, the preprocessor has already substituted #includes with the body of the header. I am playing at being my own preprocessor, doing some dangerous things like providing multiple, different definitions of a class. Later on, one of the linker's jobs is to throw out all but one of these definitions. If they do not align up in the exact right way, all kinds of bad things will happen, as you will be in the twilight zone of undefined behavior.

Note that I was careful to provide the exact same layout for my class in every compilation unit; every definition has exactly 1 int and 0 virtual methods. Note that I did not introduce any extra methods (though that might work; still doing things like this should be looked on with great suspicion), the only thing that changed were some non-virtual member functions (well constructors really) and then only to remove the default constructor. Changing and removing the default value changed nothing about the definition of A::A(int).

I don't have a copy of the spec on me, so I can't say if my careful changes fall under undefined behavior or implementation specific behavior, but I would treat it as such for production code and avoid leveraging such tricks.

And the ultimate answer to what argument is used inside of Baz is,.... 42!

like image 22
Marc Avatar answered Sep 28 '22 12:09

Marc