在考虑这个问题的时候,我偶然发现了一些我不明白的东西。
标准说..。
[class.dtor]/4 如果类没有用户声明的析构函数,则析构函数将隐式声明为默认值。隐式声明的析构函数是其类的内联公共成员。[class.dtor]/10 ..。如果一个类有一个带有虚拟析构函数的基类,那么它的析构函数(不管是用户声明的还是隐式声明的)都是虚拟的。[class.dtor]/7 默认且未定义为已删除的析构函数在odr使用时或在第一次声明后显式默认时被隐式定义。[basic.def.odr]/3 ..。如果虚拟成员函数不是纯的,则使用odr。..。
所以现在我想知道这段代码是否应该编译:
#include <memory>
struct Base {
virtual ~Base() = default;
};
struct Bar;
struct Foo : Base {
std::unique_ptr<Bar> bar_{};
};我认为~Foo()必须被隐式定义,因为它是虚拟的,但是它不会编译,因为Bar在这个TU中是不完整的。然而,代码在所有主要编译器中都会编译。
我遗漏了什么?
发布于 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?
很明显,“什么时候”并不是指物理时间,很可能意味着“哪里”。这大概意味着:
但是对于一个隐含的odr--使用一个虚拟析构函数,这完全是因为它是虚拟的,它在哪里使用呢?解释它的一种方法是,在出现类Foo定义的每个翻译单元中,Foo::~Foo被认为是odr--在该翻译单元中使用,编译器必须在该翻译单元中生成定义。如果是这样的话,那么您的程序需要一个诊断。
另一种解释它的方法是,由于标准没有定义任何发生隐含odr使用的地方,因此它被认为是未指定的,并且实现可以选择在哪里定义析构函数,甚至根本不定义。(也就是说,如果" where“指的是"where",而”where“指的是”发生odr使用的一组地方“,则没有具体说明该集合实际上是什么,而且可能是空的。)实际上,编译器可能在翻译单元中定义析构函数,其中“odr--由vtable使用”(我将其放入引号中,因为技术上标准没有定义vtable的概念,因此vtable不能真正使用odr--使用任何东西),vtable是“odr--被某些表达式使用”,例如构造函数的潜在调用、dynamic_cast、typeid和涉及Foo的异常处理。如果您不做任何这些事情,编译器就不会在TU中隐式地定义虚拟析构函数。
我认为,标准措词可能应该修改,以便编纂现有的做法,即标准应说明在什么地方发生了隐含的odr-使用虚拟函数。(而且,如果实现决定答案是“无处的”,则不应该允许它将程序视为basic.def.odr/4下格式不良的NDR,这将是错误的。)
发布于 2019-10-26 03:29:26
所以现在我想知道这段代码是否应该编译:
你是不是想知道这个完整的程序“应该”编译
void f();
void g() { auto ff = &f; }
int main() {}如果您假设它会编译,那么“多少次”您得出的结论是代码将编译?(它应该编译的是一个单一的真理,还是一个双重独立的真理?)
f()是以一种非使用的方式“使用”的(地址分配给了优化后的局部变量)。g():
retg()本身甚至没有被使用任何“哑”链接器都可以看到,因为不需要g(),所以不需要f();这是假设“哑”编译器会假设g()中甚至需要f()!
然而,(**f()**)未声明的函数显然是ODR使用的。您是期望实现行为,还是“好奇”并提出有关它的问题?
如果您预期没有错误,那么您可能应该重新考虑整个"ODR-use违规得到编译器的传递让我感到奇怪“的事情。
实现不诊断缺乏定义,他们不需要做一个工作的程序。
它们需要什么取决于实现的巧妙性,因为很明显,如果不需要f()的g()被隐藏在复杂的代码中,那么上面的论点就会变得更加复杂。我故意给出一个简单的例子,一个在-O0有效的例子。
[注意:这是g()在-O0级别的程序集
g():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], OFFSET FLAT:_Z1fv
nop
pop rbp
ret如您所见,不依赖于符号f()。
知识取决于优化级别和代码生成时提供的数据量(同时编译多个翻译单元可能会有所帮助)。
对于非虚拟函数,所需的对象(另一个函数或使用该函数地址初始化的全局对象)需要该函数(即命名)。
对于虚拟函数来说,获取知识要困难得多:知道一个重载者不会被调用,并不像对一个非重载函数(包括一个非虚拟函数)那样简单,因为当对一个(G)值进行虚拟调用时,可以通过“延迟绑定”来调用一个过载者。
编译器可能能够或可能无法精确地确定哪些虚拟函数永远不需要,这取决于程序的复杂性(停止程序问题表明它们永远不会准确地知道所有程序),但是从未明显实例化的对象的非静态成员函数并不需要。
https://stackoverflow.com/questions/58543232
复制相似问题