首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >当没有创建类的实例时,标准是否允许隐式虚拟析构函数不被隐式定义?

当没有创建类的实例时,标准是否允许隐式虚拟析构函数不被隐式定义?
EN

Stack Overflow用户
提问于 2019-10-24 14:10:32
回答 2查看 340关注 0票数 7

在考虑这个问题的时候,我偶然发现了一些我不明白的东西。

标准说..。

[class.dtor]/4 如果类没有用户声明的析构函数,则析构函数将隐式声明为默认值。隐式声明的析构函数是其类的内联公共成员。[class.dtor]/10 ..。如果一个类有一个带有虚拟析构函数的基类,那么它的析构函数(不管是用户声明的还是隐式声明的)都是虚拟的。[class.dtor]/7 默认且未定义为已删除的析构函数在odr使用时或在第一次声明后显式默认时被隐式定义。[basic.def.odr]/3 ..。如果虚拟成员函数不是纯的,则使用odr。..。

所以现在我想知道这段代码是否应该编译:

代码语言:javascript
复制
#include <memory>

struct Base {
    virtual ~Base() = default;
};

struct Bar;
struct Foo : Base {
    std::unique_ptr<Bar> bar_{};
};

https://godbolt.org/z/B0wvzd

我认为~Foo()必须被隐式定义,因为它是虚拟的,但是它不会编译,因为Bar在这个TU中是不完整的。然而,代码在所有主要编译器中都会编译。

我遗漏了什么?

EN

回答 2

Stack Overflow用户

发布于 2019-10-24 17:58:37

好的,既然这个问题已经被发布的各种答案和评论澄清了,让我再试一次回答它。引用是对C++17标准的引用。

OP代码的问题是,Foo有一个非纯的虚拟析构函数,并且所有非纯虚拟函数都隐式地使用odr (basic.def.odr/3)。由于析构函数是使用odr的,所以实现似乎必须生成一个定义(class.dtor/7),并且该定义必须导致std::default_delete<Bar>::operator()的实例化,这使得程序由于Bar是不完整的而导致格式错误(惟一的e.ptr.dltr.dflt/4)。那么为什么编译器不产生诊断呢?

我认为,class.dtor/7的措辞存在问题,其内容如下,迄今更新的标准版本中未作实质性改动:

默认且未定义为已删除的析构函数在odr使用(6.2)或第一次声明后显式默认时将被隐式定义。

Foo::~Foo的隐式定义是什么时候生成的?它是生成“当”Foo::~Foo是odr-使用。但是..。什么时候使用Foo::~Foo odr?

很明显,“什么时候”并不是指物理时间,很可能意味着“哪里”。这大概意味着:

  • 如果析构函数是由表达式使用的,则必须在表达式出现的转换单元中生成定义,并且
  • 如果析构函数是用于类模板的,那么tem.point/1也会应用。

但是对于一个隐含的odr--使用一个虚拟析构函数,这完全是因为它是虚拟的,它在哪里使用呢?解释它的一种方法是,在出现类Foo定义的每个翻译单元中,Foo::~Foo被认为是odr--在该翻译单元中使用,编译器必须在该翻译单元中生成定义。如果是这样的话,那么您的程序需要一个诊断。

另一种解释它的方法是,由于标准没有定义任何发生隐含odr使用的地方,因此它被认为是未指定的,并且实现可以选择在哪里定义析构函数,甚至根本不定义。(也就是说,如果" where“指的是"where",而”where“指的是”发生odr使用的一组地方“,则没有具体说明该集合实际上是什么,而且可能是空的。)实际上,编译器可能在翻译单元中定义析构函数,其中“odr--由vtable使用”(我将其放入引号中,因为技术上标准没有定义vtable的概念,因此vtable不能真正使用odr--使用任何东西),vtable是“odr--被某些表达式使用”,例如构造函数的潜在调用、dynamic_casttypeid和涉及Foo的异常处理。如果您不做任何这些事情,编译器就不会在TU中隐式地定义虚拟析构函数。

我认为,标准措词可能应该修改,以便编纂现有的做法,即标准应说明在什么地方发生了隐含的odr-使用虚拟函数。(而且,如果实现决定答案是“无处的”,则不应该允许它将程序视为basic.def.odr/4下格式不良的NDR,这将是错误的。)

票数 0
EN

Stack Overflow用户

发布于 2019-10-26 03:29:26

所以现在我想知道这段代码是否应该编译:

你是不是想知道这个完整的程序“应该”编译

代码语言:javascript
复制
void f();
void g() { auto ff = &f; }
int main() {}

如果您假设它会编译,那么“多少次”您得出的结论是代码将编译?(它应该编译的是一个单一的真理,还是一个双重独立的真理?)

  1. f()是以一种非使用的方式“使用”的(地址分配给了优化后的局部变量)。
代码语言:javascript
复制
g():
        ret
  1. g()本身甚至没有被使用

任何“哑”链接器都可以看到,因为不需要g(),所以不需要f();这是假设“哑”编译器会假设g()中甚至需要f()

然而,(**f()**)未声明的函数显然是ODR使用的。您是期望实现行为,还是“好奇”并提出有关它的问题?

如果您预期没有错误,那么您可能应该重新考虑整个"ODR-use违规得到编译器的传递让我感到奇怪“的事情。

实现不诊断缺乏定义,他们不需要做一个工作的程序。

它们需要什么取决于实现的巧妙性,因为很明显,如果不需要f()g()被隐藏在复杂的代码中,那么上面的论点就会变得更加复杂。我故意给出一个简单的例子,一个在-O0有效的例子。

[注意:这是g()-O0级别的程序集

代码语言:javascript
复制
g():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], OFFSET FLAT:_Z1fv
        nop
        pop     rbp
        ret

如您所见,不依赖于符号f()

知识取决于优化级别和代码生成时提供的数据量(同时编译多个翻译单元可能会有所帮助)。

对于非虚拟函数,所需的对象(另一个函数或使用该函数地址初始化的全局对象)需要该函数(即命名)。

对于虚拟函数来说,获取知识要困难得多:知道一个重载者不会被调用,并不像对一个非重载函数(包括一个非虚拟函数)那样简单,因为当对一个(G)值进行虚拟调用时,可以通过“延迟绑定”来调用一个过载者。

编译器可能能够或可能无法精确地确定哪些虚拟函数永远不需要,这取决于程序的复杂性(停止程序问题表明它们永远不会准确地知道所有程序),但是从未明显实例化的对象的非静态成员函数并不需要。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/58543232

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档