【问题标题】:Why shouldn't the inherited constructor inherit the default arguments?为什么继承的构造函数不应该继承默认参数?
【发布时间】:2015-10-18 18:11:39
【问题描述】:

C++ Primer(第 5 版)第 629 页指出:

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

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

【问题讨论】:

  • 我会冒险猜测,由于默认参数只能指定一次,那么派生类在技术上不能继承具有默认参数的构造函数,因为编译器不知道哪一组要使用的默认值。
  • 如果我理解正确,你的意思是这两个定义之间不应该有矛盾。但是这里没有矛盾是可能的......
  • 我会冒险猜测这样做可以避免在将那些默认参数初始化的参数传递给基类的构造函数时制作额外的副本
  • 你有什么问题?从具有默认参数的基本构造函数生成的派生类的继承构造函数集等效于具有默认参数的(潜在)继承构造函数,至少与直接使用有关(与链接无关,但实际上,继承的构造函数将被内联)。

标签: c++ c++11 inheritance


【解决方案1】:

鉴于当前的措辞;我认为在这些术语中指定了它(C++ WD n4527 的§12.9/1),原因很少(但主要是为了避免潜在的歧义);

  1. 避免歧义。 IE。如果您开始为自己的构造函数提供匹配的参数,这将允许这些构造函数与继承的构造函数不冲突(不明确)
  2. 保持其效果。 IE。客户端代码是什么样子的

inheriting constructors 是一种类似于代码生成的技术(“我想要我的基础拥有的东西”)。没有办法指定你得到哪些构造函数,你基本上都得到了它们,因此编译器非常注意不要生成模棱两可的构造函数。

举例说明;

#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
派生3
Base45

Sample code.


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)

简而言之,虽然可能不是最终的措辞,但似乎其意图是允许构造函数与它们的默认参数一起使用,并明确排除隐藏和覆盖的声明,因此我相信会处理任何歧义。该措辞似乎确实简化了标准,但产生了相同的结果 w.r.t.它在客户端代码中使用。

【讨论】:

  • 但是出于同样的原因,可以在 Base 中定义另一个构造函数,采用 int,这将导致类似的歧义机会。为什么标准突然决定在继承的情况下保护我?
  • @MeirGoldenberg。是的,那里没有任何变化,但这将导致编译时失败,代码显式输入(即不是编译器生成),继承构造函数是一种类似于代码生成的技术(“我想要我的基础拥有的东西”)。没有办法指定你得到哪些构造函数,你基本上都得到了它们,因此编译器非常注意不要生成模棱两可的构造函数。
【解决方案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

【讨论】:

  • 哎呀。我认为必须在函数声明中提供默认参数(在 A.h 中,在 A::A(int,int) 的情况下)并且以后不能提供(在 A.cc 中的函数定义中) - 还是我弄错了?跨度>
  • @Walter,你错了。对于构造函数模板,它们必须在声明中给出,但对于非模板构造函数则不需要。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    请注意,就像 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 page 和介绍更改(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)。

    所以参数列表现在完全“继承”了。确实,这是我在 GCC 7.2 中使用符合 P0136R1 的 CLion 的经验,而我的不符合 P0136R1 的 Visual Studio 2017 (15.6) 显示了较旧的 4 个构造函数,并且删除了默认参数。

    【讨论】:

      猜你喜欢
      • 2016-03-24
      • 1970-01-01
      • 2011-05-20
      • 1970-01-01
      • 2017-10-04
      • 2015-07-09
      • 1970-01-01
      • 2020-02-28
      • 2013-08-27
      相关资源
      最近更新 更多