AlwaysLearning
AlwaysLearning

Reputation: 8051

Why shouldn't the inherited constructor inherit the default arguments?

C++ Primer (5th edition) on page 629 states:

  • If a base class constructor has default arguments, those arguments are not inherited. Instead, the derived class gets multiple inherited constructors in which each parameter with a default argument is successively omitted.

What is the reasoning behind this rule?

Upvotes: 17

Views: 4409

Answers (4)

L.Y. Sim
L.Y. Sim

Reputation: 900

Heads up, just like what Jonathan Wakely mentioned, C++17 has since changed this behavior. Now the default arguments in the parameter list DOES get inherited.

That is to say, if we had the following constructor in a class called Base,

struct Base {
    Base(int a, int b, int c = 1, int d = 2, int e = 3) {}
};

then for that constructor above, these are corresponding ones that are 'injected' into the derived class in C++11/C++14:

struct Derived : Base {
    using Base::Base;
    /*
    C++11/C++14:
    Derived::Derived(int a, int b) : Base(a, b) {}
    Derived::Derived(int a, int b, int c) : Base(a, b, c) {}    
    Derived::Derived(int a, int b, int c, int d) : Base(a, b, c, d) {}
    Derived::Derived(int a, int b, int c, int d, int e) : Base(a, b, c, d, e) {}
    */
};

while the one in C++17 is now much simpler:

struct Derived : Base {
    using Base::Base;
    /*
    C++17:
    Derived::Derived(int a, int b, int c = 1, int d = 2, int e = 3) : Base(a, b, c, d, e) {}
    */
};

Why I think this is:

Based on the cppreference.com page on inheriting constructors and the paper that introduced the changes (P0136R1), the entire [class.inhctor]\1 subsection which specified how the inherited constructors are split up and 'injected' into the derived class was removed. (In fact the whole [class.inhctor] section was removed). It was then replaced with a simple rule in [namespace.udecl]\16 in C++17 which says (emphasis mine):

For the purpose of overload resolution, the functions that are introduced by a using-declaration into a derived class are treated as though they were members of the derived class. In particular, the implicit this parameter shall be treated as if it were a pointer to the derived class rather than to the base class. This has no effect on the type of the function, and in all other respects the function remains a member of the base class. Likewise, constructors that are introduced by a using-declaration are treated as though they were constructors of the derived class when looking up the constructors of the derived class (6.4.3.1) or forming a set of overload candidates (16.3.1.3, 16.3.1.4, 16.3.1.7). If such a constructor is selected to perform the initialization of an object of class type, all subobjects other than the base class from which the constructor originated are implicitly initialized (15.6.3).

So the parameter list now 'carries over' completely. Indeed this is my experience using the P0136R1-compliant CLion with GCC 7.2, while my non P0136R1-compliant Visual Studio 2017 (15.6) showed the older 4 constructors with the default arguments dropped.

Upvotes: 2

Niall
Niall

Reputation: 30624

Given the current wording; I think it is specified in these terms (§12.9/1 of C++ WD n4527) for few reasons (but principally to avoid potential ambiguity);

  1. Avoid ambiguity. I.e. if you start to provide your own constructors with matching parameters, this will then allow for these constructors to not conflict (be ambiguous) with the inherited constructors
  2. Maintain the effect thereof. I.e. what would the client code look like

The inheriting constructors is a technique akin to code generation ("I want what my base has"). There is no way to specify which constructors you are getting, you basically get them all hence the compiler much take care to not generate ambiguous constructors.

By way of example;

#include <iostream>
using namespace std;
struct Base {
    Base (int a = 0, int b = 1) { cout << "Base" << a << b << endl; }
};
struct Derived : Base {
    // This would be ambiguous if the inherited constructor was Derived(int=0,int=1)
    Derived(int c) { cout << "Derived" << c << endl; }
    using Base::Base;
};
int main()
{
    Derived d1(3);
    Derived d2(4,5);
}

Outputs;

Base01
Derived3
Base45

Sample code.


There is a proposal out n4429 (as noted by Jonathan Wakely) for a change in the wording around the inheriting constructors and the using declaration for classes.

Given the intent of the proposal;

... this proposal makes inheriting a constructor act just like inheriting any other base class member, to the extent possible.

There is the following change (new wording);

Change in 7.3.3 namespace.udecl paragraph 15:

When a using-declaration brings declarations from a base class into a derived class... Such hidden or overridden declarations are excluded from the set of declarations introduced by the using-declaration.

And immediately follows this with an example that deals directly the constructors (although without default arguments);

struct B1 {
  B1(int);
};

struct B2 {
  B2(int);
};

struct D1 : B1, B2 {
  using B1::B1;
  using B2::B2;
};
D1 d1(0);    // ill-formed: ambiguous

struct D2 : B1, B2 {
  using B1::B1;
  using B2::B2;
  D2(int);   // OK: D2::D2(int) hides B1::B1(int) and B2::B2(int)
};
D2 d2(0);    // calls D2::D2(int)

In short, whilst probably not the final wording, it seems that the intent is to allow the constructors to be used with their default arguments and explicitly excludes hidden and overridden declarations thus I believe taking care of any ambiguity. The wording does seem to simplify the standard, yet yielding the same result w.r.t. it being used in client code.

Upvotes: 9

songyuanyao
songyuanyao

Reputation: 173024

What is the reasoning behind this rule?

It could prevent from the changes of the default argument from base class influencing the behaviour of all the derived class (inside the derived class scope), which will be a surprise for the derived class creators.

Upvotes: 2

Jonathan Wakely
Jonathan Wakely

Reputation: 171433

Default arguments are not part of a function's signature, and can be added later, and in a restricted scope, which would not be able to change the already-defined constructors of a derived class e.g.

// in A.h
struct A {
    A(int, int);
};

// in B.h
#include "A.h"
struct B : A {
    using A::A;
};

// in A.cc
#include "A.h"
A::A(int, int = 0) { }

In the file A.cc you can construct an A with a single parameter, because the default argument is visible, but when B was declared the default argument was not visible, so cannot be considered when inheriting the constructors. I believe this is one reason that default arguments get special treatment.

Although apparently how inheriting constructors work might be going to change, and default arguments would not get this special handling, see http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4429.html

Upvotes: 6

Related Questions