C++ 为什么可以';t虚拟函数是否使用返回类型推断?
说: §7.1.6.4/14: 用使用占位符类型的返回类型声明的函数 不应是虚拟的(10.3) 因此,以下程序的格式不正确:C++ 为什么可以';t虚拟函数是否使用返回类型推断?,c++,c++14,auto,virtual-functions,vtable,C++,C++14,Auto,Virtual Functions,Vtable,说: §7.1.6.4/14: 用使用占位符类型的返回类型声明的函数 不应是虚拟的(10.3) 因此,以下程序的格式不正确: struct s { virtual auto foo() { } }; 我所能找到的理由是这句模糊的一行: 虚拟的 可以允许虚拟机的返回类型扣除 函数,但这会使覆盖检查和vtable都变得复杂 布局,所以似乎最好禁止这种情况 有人能提供进一步的理由或给出一个与上述引文一致的好(代码)示例吗?您所包含的理由相当清楚:自然地,虚拟函数意味着被子类覆
struct s
{
virtual auto foo()
{
}
};
我所能找到的理由是这句模糊的一行:
虚拟的
可以允许虚拟机的返回类型扣除
函数,但这会使覆盖检查和vtable都变得复杂
布局,所以似乎最好禁止这种情况
有人能提供进一步的理由或给出一个与上述引文一致的好(代码)示例吗?您所包含的理由相当清楚:自然地,虚拟函数意味着被子类覆盖,因此,作为基类的设计者,您应该让继承您的类的人尽可能容易地提供合适的重写。但是,如果您使用
auto
,那么计算重写的返回类型对于程序员来说是一项乏味的任务。编译器的问题会少一些,但人类会有很多机会感到困惑
例如,如果您看到如下所示的return语句
return a * 3 + b;
您必须将程序追溯到a
和b
的声明点,找出促销类型,并决定退货类型
语言设计者似乎意识到这相当混乱,并决定不允许使用此功能。好吧,函数的推导返回类型只有在函数定义时才会被知道:返回类型是从函数体内部的
return
语句推导出来的
同时,构建vtable并纯粹基于类定义中的函数声明检查覆盖语义。这些检查从不依赖函数定义,也不需要查看定义。例如,该语言要求重写函数与其重写的函数具有相同的返回类型或协变返回类型。当非定义函数声明指定一个推导的返回类型(即不带尾随返回类型的auto
)时,其返回类型在该点是未知的,并且在编译器遇到函数定义之前保持未知。当返回类型未知时,无法执行上述返回类型检查。如果要求编译器以某种方式将返回类型检查推迟到已知的程度,则需要对语言规范的这一基本领域进行重大的定性重新设计。(我甚至不确定这是否可能。)
另一种选择是,根据“无需诊断”或“行为未定义”的总体要求,减轻编译器的负担,即将责任移交给用户,但这也将构成对语言先前设计的重大偏离
基本上,出于某种类似的原因,您不能将
&
运算符应用于声明为auto f()的函数代码>但尚未定义,如7.1.6.3/11中的示例所示。auto
是类型方程中的未知类型;通常,应该在某个点定义类型。一个虚拟函数需要有一个定义,它总是被“使用”的,即使该函数从未在程序中被调用
vtable问题的简短描述
协变返回类型是vtable的一个实现问题:协变返回是一个内部强大的特性(然后被任意语言规则阉割)。协方差仅限于派生到基类转换的指针(和引用),但其内在动力和实现难度几乎是任意转换的一种:派生到基类等于任意代码(派生到基类仅限于独占的基类子对象,也称为非虚拟继承,要简单得多)
转换为共享基子对象(也称为虚拟继承)时的协方差意味着,在一般情况下,转换不仅可以更改指针的值表示形式,还可以以信息丢失的方式更改其值
因此,虚拟协方差(涉及虚拟继承转换的协方差返回类型)意味着在主基情况下不能将重写器与被重写函数混淆
详细说明
vtables的基本理论和主要基础
Primbase
是这里的主基,它从派生对象的同一地址开始。这一点非常重要:对于主基,上/下转换可以在生成的代码中通过重新解释或C样式转换来完成。单一继承对于实现者来说要容易得多,因为只有主基类。对于多重继承,需要使用指针算法
Der
中只有一个vptr,Primbase
中的一个;Der
有一个vtable,布局与Primbase
的vtable兼容
这里,通常的编译器不会为vtable中的Der::foo()
分配另一个插槽,因为派生函数实际上是用Primbase*
这个指针而不是Der*
调用的(在生成的C代码中)。Der
vtable只有两个插槽(加上RTTI数据)
主协方差
现在我们添加一些简单的协方差:
struct Primbase {
virtual Primbase *foo(); // new slot in vtable
};
struct Der
: Primbase { // primary base
Der *foo(); // replaces Primbase::foo() in vtable
virtual void bar(); // new slot
};
这里的协方差是微不足道的,因为它涉及一个主基。在编译的代码级别上看不到任何内容
非零偏移协方差
更复杂的是:
struct Basebelow {
virtual void bar(); // new slot
};
struct Primbase {
virtual Basebelow *foo(); // new
};
struct Der
: Primbase, // primary base
Basebelow { // base at a non zero offset
Der *foo(); // new slot?
};
这里,Der*
的表示形式与其基类子对象指针basedown*
的表示形式不同。两种实现选择:
- (结算)结算到
baselower*(Primbase::foo)(
虚拟调用接口fo
struct Basebelow {
virtual void bar(); // new slot
};
struct Primbase {
virtual Basebelow *foo(); // new
};
struct Der
: Primbase, // primary base
Basebelow { // base at a non zero offset
Der *foo(); // new slot?
};
struct B {};
struct L : virtual B {};
struct R : virtual B {};
struct D : L, R {};
struct B {
virtual int f();
virtual B *g();
};
struct D : B {
auto f(); // int f()
auto g(); // ?
};
struct D2 : D {
T1 f(); // T1 must be int
T2 g(); // ?
};
auto D::g() {
return new D;
} // covariant D* return