Почему унаследованный конструктор не наследует аргументы по умолчанию?

С++ Primer (5-е издание) на стр. 629:

  • Если конструктор базового класса имеет аргументы по умолчанию, эти аргументы не наследуются. Вместо этого производный класс получает несколько наследуемых конструкторов, в которых каждый параметр с аргументом по умолчанию последовательно опускается.

Какова причина этого правила?

Ответ 1

Учитывая текущую формулировку; Я думаю, что это указано в этих терминах (§12.9/1 С++ WD n4527) по нескольким причинам (но главным образом, чтобы избежать потенциальной двусмысленности);

  • Избегайте двусмысленности. То есть если вы начнете предоставлять свои собственные конструкторы с соответствующими параметрами, это позволит этим конструкторам не конфликтовать (быть неоднозначными) с унаследованными конструкторами
  • Поддерживайте его. То есть что бы выглядел код клиента

наследующие конструкторы - это метод, похожий на генерацию кода ( "Я хочу, что у моей базы есть" ). Невозможно указать, какие конструкторы вы получаете, вы в основном получаете их все, поэтому компилятор очень заботится о том, чтобы не генерировать неоднозначные конструкторы.

В качестве примера:

#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);
}

Выходы;

Base01
Derived3
Base45

Пример кода.


Существует предложение n4429 (как отметил Джонатан Вакели) для изменения формулировки вокруг наследующих конструкторов и использования объявление для классов.

Учитывая намерение предложения;

... это предложение делает наследование действия конструктора так же, как наследование любого другого элемента базового класса, насколько это возможно.

Существует следующее изменение (новая формулировка);

Изменить в 7.3.3 namespace.udecl пункт 15:

Когда декларация using приносит объявления из базового класса в производный класс... Такие скрытые или переопределенные объявления исключаются из набора объявлений, введенных с помощью объявления-объявления.

И сразу следует за ним с примером, который напрямую обрабатывает конструкторы (хотя и без аргументов по умолчанию);

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)

Короче говоря, хотя, вероятно, и не окончательная формулировка, кажется, что цель состоит в том, чтобы позволить конструкторам использоваться с аргументами по умолчанию и явно исключает скрытые и переопределенные объявления, таким образом, я считаю, что заботясь о любой двусмысленности. Формулировка, похоже, упрощает стандарт, но при этом дает тот же результат w.r.t. он используется в клиентском коде.

Ответ 2

Аргументы по умолчанию не являются частью сигнатуры функции и могут быть добавлены позже и в ограниченной области, которая не сможет изменить уже определенные конструкторы производного класса, например.

// 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) { }

В файле A.cc вы можете построить A с единственным параметром, потому что аргумент по умолчанию виден, но когда объявлен B, аргумент по умолчанию не был видимым, поэтому его нельзя рассматривать при наследовании конструкторов, Я считаю, что это одна из причин того, что аргументы по умолчанию получают специальное обращение.

Хотя очевидно, что работа над наследующими конструкторами может измениться, а аргументы по умолчанию не будут получать эту специальную обработку, см. http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4429.html

Ответ 3

Какова причина этого правила?

Это может помешать изменению аргумента по умолчанию из базового класса, влияющего на поведение всего производного класса (внутри области производного класса), что станет неожиданностью для создателей производных классов.

Ответ 4

Направляется вверх, точно так же, как упоминал Джонатан Вакли, С++ 17 с тех пор изменил это поведение. Теперь аргументы по умолчанию в списке параметров ДОЛЖНЫ наследоваться.

То есть, если бы у нас был следующий конструктор в классе с именем Base,

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

то для этого конструктора, приведенного выше, это соответствующие, которые "вводятся" в производный класс в С++ 11/С++ 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) {}
    */
};

в то время как один в С++ 17 теперь намного проще:

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) {}
    */
};

Почему я так думаю:

На основе страницы cppreference.com о наследовании конструкторов и документе, в котором были внесены изменения (P0136R1), весь подраздел [class.inhctor]\1, который указал, как унаследованные конструкторы разделяются и "впрыскивается" в производный класс, удаляется. (Фактически весь раздел [class.inhctor] удален). Затем он был заменен простым правилом в [namespace.udecl]\16 в С++ 17, в котором говорится (выделено мной):

В целях разрешения перегрузки функции, которые вводятся с помощью декларации использования в производную класс рассматриваются так, как если бы они были членами производного класса. В частности, неявный этот параметр обрабатывается так, как если бы это был указатель на производный класс, а не на базовый класс. Это не влияет на тип функции, и во всех остальных отношениях функция остается членом базового класса. Точно так же, Конструкторы , которые вводятся с помощью объявления-объявления, рассматриваются так, как если бы они были конструкторами производный класс при поиске конструкторов производного класса (6.4.3.1) или формировании набора перегрузок кандидатов (16.3.1.3, 16.3.1.4, 16.3.1.7). Если такой конструктор выбран для выполнения инициализации объект типа класса, все подобъекты, отличные от базового класса, из которого был создан конструктор, являются неявно инициализируется (15.6.3).

Итак, список параметров теперь полностью переносится. На самом деле, это мой опыт использования CLIO с P0136R1 с GCC 7.2, в то время как моя совместимая с Visual Studio 2017 (15.6), не совместимая с P0136R1, показала более старые 4 конструктора с отключенными аргументами по умолчанию.