前段时间我用一组嵌套类编写了一些代码。今天看一下,我想知道为什么在实例化一个C类型的对象时它会编译。
我的困惑是这个。 B有一个私有构造函数。这个构造函数的实现负责构造A,但是A的构造需要B的实例。我觉得它是鸡和鸡蛋的场景。 B的构造需要构建A,这需要无限制地构建B等。
我有以下课程 - 正好剥离以演示问题:
// ******* A ******** //
class A {
public:
A(A& _Parent, int id);
private:
A& Parent;
};
inline A::A(A& _Parent, int id)
: Parent(_Parent)
{
}
// ******* B ******** //
class B:public A {
public:
static B& GetInstance();
private:
B();
};
inline B::B()
: A(B::GetInstance(), 0)
{
}
inline B& B::GetInstance()
{
static B b;
return b;
}
// ******* C ******** //
class C:public A {
public:
C();
};
inline C::C()
: A(B::GetInstance(), 0)
{
}
答案 0 :(得分:2)
缩小问题范围:
inline B& B::GetInstance()
{
static B b;
return b;
}
inline B::B()
: A(B::GetInstance(), 0)
{
}
第一次调用函数时,行static B b;
会创建一个B
对象。但是,构建b
然后调用GetInstance
,static B b;
到达b
,而{{1}}仍在构建中。
此案例由C ++ 14 [stmt.dcl] / 4:
涵盖[...]这个变量在初始化完成后被认为是初始化的。 [...]如果控件在初始化变量时递归地重新输入声明,行为未定义。
我删除了一个部分,讨论如果抛出异常会发生什么,或者两个不同的线程同时尝试初始化静态变量。该标准允许在第一次调用函数之前初始化局部静态变量,但即使实现这样做,控制也会以递归方式重新输入声明时出现同样的问题。
未定义的行为,任何事情都可能发生。一个可能的结果是它似乎按预期工作。该标准不要求在编译时对其进行诊断 - 这是一个难以分析的问题。如注释中所示,gcc的一个版本在运行时检测到这种情况并引发异常。也许你的原始编译器通过一个标志来实现本地静态,指示执行是否已到达该行,并在调用构造函数之前设置标志。
在标准中使用未定义行为的基本原理是避免对实现实现明确定义的行为的方式施加约束(在这种情况下,确保本地静态仅初始化一次的方法)。
当然,您应该找到一种方法来解决问题,因为未来未定义的行为可能会以难以预测的方式表现出来。
答案 1 :(得分:1)
虽然您没有显示任何此类代码,但我们假设某些内容尝试调用C
的默认构造函数或以其他方式调用B::GetInstance()
。
GetInstance()
中的第一个声明是static B b;
。由于这是我们第一次到达此处,因此需要通过调用默认构造函数b
来初始化对象B::B()
。
B::B()
做的第一件事是调用GetInstance()
,然后打算将结果传递给A::A(A&, int)
构造函数。
这使我们再次回到static B b;
陈述。 C ++标准说([stmt.dcl] / 4):
第一次控件通过其声明时,将执行具有静态存储持续时间([basic.stc.static])或线程存储持续时间([basic.stc.thread])的块范围变量的动态初始化。这样的变量在初始化完成时被认为是初始化的....如果控件在初始化变量时递归地输入声明,则行为是未定义的。
未定义的行为意味着任何事情都可能发生。它可能看似有效,可能会崩溃或挂起您的程序,或者它可能会错误地初始化并继续。