为什么继承的构造函数不应该继承默认参数?

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

本文关键字:继承 参数 默认 构造函数 为什么 不应该      更新时间:2023-10-16

c++ Primer (5th edition)第629页说明:

  • 如果基类构造函数有默认实参,这些实参不会被继承。相反,派生类获得多个继承的构造函数,其中每个带有默认实参的形参都被依次省略。

这个规则背后的原因是什么?

考虑到当前的措辞;我认为在这些术语中(c++ WD n4527§12.9/1)有几个原因(但主要是为了避免潜在的歧义);

  1. 避免歧义。也就是说,如果你开始提供自己的带有匹配参数的构造函数,这将允许这些构造函数不与继承的构造函数冲突(有歧义)
  2. 保持其效果。例如,客户端代码应该是什么样子

继承构造函数是一种类似于代码生成的技术("我想要我的基所拥有的")。没有办法指定你要得到哪些构造函数,你基本上得到了所有的构造函数,因此编译器要小心不要生成歧义的构造函数。

举例;

#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中有一个提议(由Jonathan Wakely注意到),该提议改变了围绕继承构造函数和类的using声明的措辞。

考虑到提案的意图;

…在可能的范围内,继承构造函数就像继承任何其他基类成员一样。

有以下变化(新的措辞);

在7.3.3 namespace更改。Udecl第15段:

using-declaration将声明从基类引入派生类时…这些隐藏的或覆盖的声明被排除在using-declaration引入的声明集合之外。

紧接着是一个直接处理构造函数的例子(尽管没有默认参数);

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 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

这个规则背后的原因是什么?

它可以防止基类的默认参数的更改影响所有派生类的行为(在派生类范围内),这对于派生类的创建者来说将是一个惊喜

提醒一下,就像Jonathan Wakely提到的, c++ 17已经改变了这个行为。现在,形参列表中的默认实参将被继承。

也就是说,如果我们在一个名为Base的类中有以下构造函数,

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

那么对于上面的构造函数,这些是在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) {}
    */
};

而c++ 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]部分被删除)。然后在c++ 17的[namespace.udecl]16中被替换为一个简单的规则,它说(强调我的):

为了实现重载解析,使用using声明将函数引入派生类类被视为派生类的成员。特别是隐式this参数应被视为指向派生类而不是基类的指针。这对…没有影响函数的类型,以及在所有其他方面,函数仍然是基类的成员。同样的,由using声明引入的构造函数将被视为类的构造函数当查找派生类的构造函数(6.4.3.1)或形成一组重载时候选人(16.3.1.3, 16.3.1.4 16.3.1.7)。如果选择这样的构造函数来执行初始化类类型的对象,除了派生构造函数的基类以外的所有子对象都是隐式初始化(15.6.3)。

所以参数列表现在完全"延续"。事实上,这是我使用符合p0136r1的CLion与GCC 7.2的经验,而我的非p0136r1兼容的Visual Studio 2017(15.6)显示了旧的4构造函数,默认参数被删除。