让我们考虑以下代码片段(分布在多个文件中):
Base.h:
toggle
Derived.h:
#include <iostream>
class Base
{
public:
virtual void foo () = 0;
};
template <class D>
class BaseImpl : public Base
{
public:
BaseImpl () : d (new D ()) {}
virtual ~BaseImpl () { delete d; }
void foo () override;
private:
D * d;
};
template <class D>
void BaseImpl <D>::foo ()
{
std::cout << d->value << std::endl;
}
Derived.cpp:
#include "Base.h"
struct Data;
class Derived : public BaseImpl <Data>
{
public:
Derived ();
~Derived ();
};
main.cpp:
#include "Derived.h"
struct Data
{
int value = 123;
};
Derived::Derived () {}
Derived::~Derived () {}
此示例在第[1]行的CLang(Apple LLVM版本10.0.0(clang-1000.11.45.5))上产生编译器错误,如下所示:
#include "Derived.h" int main () { Derived d; d.foo (); // [1] error ((Base *) & d)->foo (); // [2] fine }
gcc 4.9.2会生成类似的错误:
g++ -c -std=c++11 main.cpp -o main.o In file included from main.cpp:1: In file included from ./Derived.h:1: ./Base.h:22:17: error: member access into incomplete type 'Data' std::cout << d->value << std::endl; ^ main.cpp:7:5: note: in instantiation of member function 'BaseImpl<Data>::foo' requested here d.foo (); // [1] ^ ./Derived.h:3:8: note: forward declaration of 'Data' struct Data; ^ 1 error generated.
足够有趣的是,如果注释掉第[1]行,则一切正常(并且可以正常运行),第[2]行不会产生任何编译器错误。这种差异的原因是什么?
如果将g++ -c -std=c++11 main.cpp -o main.o
In file included from Derived.h:1:0,
from main.cpp:1:
Base.h: In instantiation of 'void BaseImpl<D>::foo() [with D = Data]':
main.cpp:7:10: required from here
Base.h:22:13: error: invalid use of incomplete type 'struct Data'
std::cout << d->value << std::endl;
^
In file included from main.cpp:1:0:
Derived.h:3:8: error: forward declaration of 'struct Data'
struct Data;
^
中Data
中的Derived.h
的前向声明替换为Derived.cpp
中的完整定义,则[1]处的错误将消失,但其目的是使其对用户隐藏Derived
之所以如此,是因为它是实现细节(另请参见下文)。
对于那些好奇(并熟悉Qt的人)来说,现实生活中Base
是QAbstractItemModel
,BaseImpl
是一个类模板,其目的是为模型项提供实际存储并实现{ {1}}(纯)虚函数。 QAbstractItemModel
只是实现特定数据模型的Derived
的别名,BaseImpl
是该模型内部用于存储每一项数据的数据,因此应与Data
分开并使用最终模型类将其隐藏在代码中。
答案 0 :(得分:0)
示例程序格式错误。
首先,将像检查所有其他代码一样检查任何模板的定义是否具有有效的语法,但是除非/直到实例化该模板,否则不需要C ++实现来检查依赖于模板参数的任何部分的语义。 (即使没有任何实例化实例,实现也可能报告语义错误,即使没有任何实例化也是如此。)
因此,如果main.cpp生成的翻译单元未实例化功能模板特化BaseImpl<Data>::foo()
,则该程序会很好。但是,何时确切地实例化此模板专业化? [temp.inst]/3说:
除非类模板或成员模板的成员已被显式实例化或显式专门化,否则当在需要存在成员定义的上下文中引用专门化或如果存在成员的情况下隐式实例化成员的专门化。成员的定义会影响程序的语义。
存在定义的主要要求来自ODR规则[basic.def.odr]/10:
每个程序应在被丢弃的语句之外,确切地包含该程序中奇特使用的每个非内联函数或变量的一个定义;不需要诊断。...内联函数或变量应在被丢弃的语句之外被奇数使用的每个翻译单元中定义。
在这里,我们说专业化BaseImpl<Data>::foo()
是否为“内联”都没有关系。两种ODR规则最终都表明,如果过度使用了专门化,则必须(以某种方式)对其进行定义,这意味着专门化了隐式实例化。
对于不同种类的实体,技术术语“ odr-use”有不同的定义(有时这些定义重叠)。引用[basic.def.odr] / 5-8的报价,以说明问题:
变量
x
的名称以可能评估的表达式ex
的形式出现,ex
被-使用除非......如果结构化绑定显示为可能评估的表达式,则将被使用。
如果*this
出现为可能评估的表达式(包括作为非静态成员函数的主体中的隐式转换的结果),则
this
是有用的。如果虚拟成员函数不是纯函数,则将使用它。如果一个函数由一个可能求值的表达式命名,则它会被使用。类的非布局分配或释放函数由该类的构造函数的定义使用。类的非放置释放函数可以通过该类的析构函数的定义来使用,或者通过在虚拟析构函数的定义点进行查找来选择。
一个类中的赋值运算符函数由[class.copy.assign]中指定的另一个类的隐式定义的复制赋值或移动赋值函数来使用。按照[dcl.init]中指定的方式使用类的构造函数。如果某个类的析构函数有可能被调用,则将使用它。
请注意,所有这些“ odd用过的”定义都将属性与某些特定的表达式或定义相关联, 是一个不纯的虚函数仅由现有函数使用的说法。因此,在大多数情况下,由于这些特定的其他表达式和定义需要模板专门化,因此发生隐式实例化。在[temp.inst]/10和[temp.point]/5中已经清楚地说明了实例化虚函数的含义:
如果未以其他方式实例化虚拟成员函数,则不确定实现是否隐式实例化类模板的虚拟成员函数。
如果虚拟函数被隐式实例化,则其实例化点紧随其封闭类模板专业化的实例化点。
因此,如果您的main.cpp根本没有提到foo
,但是它仍然需要实例化BaseImpl<Data>
类,而Data
仍是不完整的类型,则将不确定是否或程序无效!因此,即使那样也可能不是一个好主意。
尽管如此,这也是为什么允许Derived.cpp的转换单元实例化BaseImpl<Data>::foo()
的原因。而且,g ++以及大多数对虚拟函数使用通用“ vtable”策略的编译器将实例化任何需要的类模板成员,这些成员是该类的任何构造函数(显式或隐式)定义的最终替代,因为它需要构建一个包含这些函数的vtable,以便构造函数可以将类对象的内部vptr设置为指向vtable。
但是我在一开始就说它是不正确的,因为另一部分隐含着“否则将无法实例化”。可能暗示使用BaseImpl<Data>::foo()
的另一句话是“如果函数由潜在赋值的表达式命名,则会使用odr”。在引用前不久进行备份,我们发现[basic.def.odr]/3中的“由表达式命名的函数”也是技术定义:
一个函数由一个表达式命名,如下所示:
一个名称出现在表达式中的函数,如果它是唯一的查找结果或一组重载函数([basic.lookup],[over.match],[ ),除非它是一个纯虚函数且其名称未明确限定或表达式形成指向成员([expr.unary.op])的指针。
...
现在,表达式d.foo()
在foo
中查找成员Derived
,唯一结果为BaseImpl<Data>::foo()
。由于它不是纯虚拟的,所以d.foo()
名称 BaseImpl<Data>::foo()
。表达式((Base *) & d)->foo()
在foo
中查找成员Base
,唯一结果为Base::foo()
。由于Base::foo()
是纯虚拟的,并且表达式中的名称foo
没有限定,因此((Base *) & d)->foo()
不会命名任何函数。
我确定您知道这两个表达式(如果将程序更改为有效的话)实际上会调用同一函数。但是在C ++标准中描述多态的方式是命名为的函数并不总是与被称为的函数相同。由于其他原因,这种区别也很重要-例如成员访问检查适用于命名函数而不是调用函数,并且可以使用与命名函数(而不是调用函数)的声明关联的默认参数。碰巧,这取决于命名的实际函数,还会出现另一个棘手的区别。
无论如何,要绑定它,main.cpp odr中的表达式d.foo()
使用BaseImpl<Data>::foo()
,这意味着BaseImpl<Data>::foo()
的隐式实例。由于d
指向不完整类类型Data
的对象,因此该实例化专业化定义中的表达式d->value
格式错误。