构造函数继承和直接成员初始化

24

我正在尝试使用C++11直接数据成员初始化和 "using" 语法来继承基类的构造函数。现在在gcc 5.4.0(Ubuntu 16.04)下,如果数据成员类型没有默认构造函数,则会出现奇怪的错误。最简单的例子如下:

#include <iostream>

struct Foo {
  Foo(int arg) { std::cout << "Foo::Foo(" << arg << ")" << std::endl; }
};

struct Base {
  Base(int arg) { std::cout << "Base::Base(" << arg << ")" << std::endl; }
};

struct Derived : public Base {
  using Base::Base;
  Foo foo{42};
};

int main() {
  Derived derived{120};
}

这段代码在clang下编译和执行时表现符合预期。但在gcc下无法编译,因为编译器删除了构造函数Derived::Derived(int)

ttt.cpp: In functionint main()’:
ttt.cpp:17:22: error: use of deleted functionDerived::Derived(int)’
   Derived derived{120};
                      ^
ttt.cpp:12:15: note: ‘Derived::Derived(int)’ is implicitly deleted because the default definition would be ill-formed:
   using Base::Base;
               ^
ttt.cpp:12:15: error: no matching function for call toFoo::Foo()’
ttt.cpp:4:3: note: candidate: Foo::Foo(int)
   Foo(int arg) { std::cout << "Foo::Foo(" << arg << ")" << std::endl; }
   ^
ttt.cpp:4:3: note:   candidate expects 1 argument, 0 provided
ttt.cpp:3:8: note: candidate: constexpr Foo::Foo(const Foo&)
 struct Foo {
        ^
ttt.cpp:3:8: note:   candidate expects 1 argument, 0 provided
ttt.cpp:3:8: note: candidate: constexpr Foo::Foo(Foo&&)
ttt.cpp:3:8: note:   candidate expects 1 argument, 0 provided

如果我像这样为Foo添加默认构造函数:

  Foo() { std::cout << "Foo::Foo()" << std::endl; };

同时gcc也可以编译它。代码的行为完全相同,尤其是Foo的默认构造函数未被执行。

所以我的问题是,这是有效的C++11吗?如果是,那么我可能已经发现了gcc的一个bug。否则,gcc和clang都不应该给我一个错误消息,说明这不是有效的C++11吗?

在@vlad-from-moscow很好地回答了我的问题后进行编辑:这个bug似乎也存在于gcc 6.2中,所以我将提交一个bug报告。

第二次编辑:我没有在第一次搜索中找到的一个bug已经存在:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67054


我在像cppreference.com这样的页面上找不到任何关于这个的信息。那里描述了“using”语法和大括号初始化器,但没有提到如何将两者结合起来。 - Martin Hierholzer
2个回答

11
gcc不满足C++标准。类Derived的继承构造函数应该在其mem-initializer列表中调用Base构造函数,并使用Derived继承构造函数指定的参数。
C++标准中写道(12.9 继承构造函数):
当odr-used(3.2)创建其类类型的对象(1.8)时,该类的继承构造函数会被隐式定义。一个隐式定义的继承构造函数执行了该类的初始化集合,这个集合是由一个带有mem-initializer-list的用户编写的内联构造函数执行的,其中只有一个mem-initializer具有命名嵌套名称说明符中所指定的基类的mem-initializer-id和下面指定的表达式列表,它的函数体中的复合语句为空(12.6.2)。如果那个用户编写的构造函数是不合法的,那么程序也是不合法的。表达式列表中的每个表达式都是static_cast(p),其中p是相应构造函数参数的名称,T是p的声明类型。
另外根据章节12.6.2 初始化基类和成员:
在非委托构造函数中,如果某个非静态数据成员或基类没有被mem-initializer-id指定(包括构造函数没有ctor-initializer列表的情况),并且该实体不是抽象类的虚基类(10.4), 那么
- 如果实体是具有大括号或等于初始化程序的非静态数据成员,则按8.5指定的方式初始化该实体;

@VladfromMoscow:问题不在于Derived(120)没有调用Base(120),而是错误地假设表达式列表将是foo()(无效),即使提供了初始化程序,实际调用也将是foo(42)。如果删除foo数据成员,则gcc将接受该代码。 - Matthieu M.
@MatthieuM。我已经附上了我的答案。但是我发现标准的各个版本之间存在矛盾。 - Vlad from Moscow

5

看起来你是对的,gcc存在一个bug。

根据§12.9 [class.inhctor]:

命名构造函数的using声明(7.3.3)隐式声明了一组继承构造函数。在using声明中命名的类X的继承构造函数候选集由实际构造函数和以下方式从默认参数转换而来的概念构造函数组成:

  • X的所有非模板构造函数

这意味着你的Derived类应该从其基类那里获得一个接受int的构造函数。遵循类内成员初始化的正常规则,在没有Foo的默认构造函数的情况下构造Derived的实例不应该是个问题,因为它没有被使用。因此,gcc存在一个bug:

§13.3.1.3 Initialization by constructor [over.match.ctor]

当直接初始化类类型的对象时(8.5)[...],重载解析选择构造函数。对于直接初始化,候选函数是正在初始化的对象所属类的所有构造函数

因此,构造函数Foo::Foo(int)应该被选中,但在gcc中显然没有被选中。


我在阅读这篇文章后有一个问题:“这是否会导致Derived的默认构造函数被删除?”答案是不会。

方便地,标准在这个摘录下面提供了一个示例(我省略了不需要的内容):

struct B1 {
   B1(int);
};

struct D1 : B1 {
   using B1::B1;
};

D1中存在的构造函数为[Emphasis是我的]

  • D1(),隐式声明的默认构造函数,如果odr-used则形式不正确
  • D1(const D1&),隐式声明的复制构造函数,不会被继承
  • D1(D1&&),隐式声明的移动构造函数,不会被继承
  • D1(int),隐式声明的继承构造函数
(Note: "odr-used" refers to a term in C++ programming language, which means that the object is used in a way that requires it to have an address.)

2
这与Derived或Base的默认构造函数无关。gcc需要一个Foo的默认构造函数,但它实际上从未调用它。 - Martin Hierholzer
@MartinHierholzer:你说:“这段代码在clang下编译并执行时表现符合预期。但在gcc下无法编译,因为编译器删除了Derived的构造函数。” 这是gcc的错误行为,我正在解决这个问题。你不需要再做任何事情。 - AndyG
1
关于您的编辑:这仍然不涉及Derived的默认构造函数是否被删除。我对Derived的默认构造函数不感兴趣,它从未被使用过,gcc也从未抱怨过没有它。相反,gcc删除了所需的构造函数Derived :: Derived(int),因为Foo的默认构造函数不存在! - Martin Hierholzer
@MartinHierholzer:我做了更多的修改以增加清晰度。我认为我不能再提出更强有力的论点了。 - AndyG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接