私有类成员延迟初始化的最佳实践

5

如何最佳实践延迟初始化类 C 的私有成员变量 M?例如:

class C {
public:
    C();

    // This works properly without m, and maybe called at any time,
    // even before startWork was called.
    someSimpleStuff();

    // Called single time, once param is known and work can be started.
    startWork(int param); 

    // Uses m. Called multiple times.
    // Guaranteed to only be called after startWork was called 
    doProcessing(); 

private:
    M m;
};

class M {
    M(int param);
};

由于 M 没有默认的初始化程序,因此无法构造类 C 的对象。

如果您可以修改 M 的实现,则可以向 M 添加一个 init 方法,并使其构造函数不接受任何参数,这将允许构造类 C 的对象。

如果不能修改,则可以使用 std::unique_ptrC 的成员 m 包装起来,并在可能的情况下进行构造。

然而,这两种解决方案都容易出现运行时错误。是否有某种方法可以确保在编译时之前,m 只能在初始化后使用?

限制:类 C 的对象被传递给外部代码,该代码利用其公共接口,因此无法将 C 的公共方法拆分为多个类。


通过使用一个包装类,使用uniqe_ptr和get()函数,您可以确保它在未初始化之前不会被使用。但是,您无法确保您的代码不会尝试这样做,因此您仍然受限于运行时错误或默认参数。 - Anedar
2
为什么需要使用延迟初始化?你打算什么时候这样做?m是否需要成为一个成员?从你的示例代码中看来,每次想在C中进行一些“工作”时都要传递param - Simon Kraemer
@SimonKraemer 澄清了代码示例。 - Danra
这可能(或可能不)与此相关:https://dev59.com/zZPfa4cB1Zd3GeqPKfkp#35339553 - Galik
5个回答

5
最佳实践是永远不要使用延迟初始化。
在您的情况下,放弃默认构造函数C并将其替换为C(int param) : m(param){}。也就是说,类成员在构造时使用基本成员初始化进行初始化。
使用延迟初始化意味着对象可能处于未定义状态,并且实现诸如并发之类的事情更加困难。

很不幸,这并非总是可能的。例如,参数只有在调用startWork时才真正知道,但对象在此之前已经存在,并且具有一些有限的公共功能。可以将m重构为C的内部类D,并使该内部类具有非默认构造函数 - 但这并不能解决问题,只是将其代理给CD成员。 - Danra
@rozina 是的,我有。修改后的示例强调了这一点。 - Danra
在这种情况下,额外的功能可以封装在另一个类中,该类在构造时接收已经存在的对象。 - Bathsheba
1
@Danra 当您第二次调用startWork()时会发生什么?您会如何处理M?您可以只创建一个新的并丢弃旧的吗? - rozina
@rozina 是的,你可以。 - Danra
显示剩余3条评论

2
#define ENABLE_THREAD_SAFETY

class C {
public:
    C();

    // This works properly without m, and maybe called at any time,
    // even before startWork was called.
    someSimpleStuff();

    // Called single time, once param is known and work can be started.
    startWork(int param); 

    // Uses m. Called multiple times.
    // Guaranteed to only be called after startWork was called 
    doProcessing(); 

    M* mptr()
    {
#ifdef ENABLE_THREAD_SAFETY
       std::call_once(create_m_once_flag, [&] {
          m = std::make_unique<M>(mparam);
       });
#else
        if (m == nullptr)
          m = std::make_unique<M>(mparam);
#endif
       return m.get();
    }
private:
    int mparam;
    std::unique_ptr<M> m;
#ifdef ENABLE_THREAD_SAFETY
    std::once_flag create_m_once_flag;
#endif
};

class M {
    M(int param);
};

现在您只需要停止直接使用m,而是通过mptr()访问它。这样做将仅在首次使用时创建M类。


线程安全怎么办?我怀疑这个设计不是线程安全的。 - King Thrushbeard
@KingThrushbeard 我已经修改了它并添加了(可选的)线程安全性。 - Jts

1

我会选择使用unique_ptr... 您认为有什么问题吗?当使用M时,您可以轻松检查:

if(m)
    m->foo();

我知道这不是编译时检查,但据我所知,目前的编译器没有进行检查的可能性。代码分析必须相当复杂才能看到这样的问题,因为你可以在任何方法中或者(如果是公共/受保护的)甚至在另一个文件中初始化m。编译时检查意味着延迟初始化是在编译时完成的,但延迟初始化的概念本质上是基于运行时的。


这是明显的缺点。不过,如果你需要延迟初始化,我仍然认为这是最好的方法。但是,并不是所有情况下都能避免这种情况。 - IceFire
是的,但人们在方法中添加“只是一个if”,并没有意识到他们引入了一个状态,并且需要在每个代码位中管理它。但当延迟初始化时,这是不可避免的。只是强调一下。无论如何+1 ;) - neuro

1

根据我对你问题的理解,这是否是一个解决方案?

您将不需要M的功能放入D类中。您创建D对象并使用它。一旦您需要M并且想要执行doProcessing()代码,您创建C对象,将D传递给它,并使用您现在拥有的param进行初始化。

以下代码仅用于说明这个想法。在这种情况下,您可能不需要startWork()作为单独的函数,并且其代码可以编写在C的构造函数中。

注意:我已将所有函数设置为空,以便我可以编译代码以检查语法错误 :)

class M
{
public:
    M(int param) {}
};

class D
{
public:
    D() {}

    // This works properly without m, and maybe called at any time,
    // even before startWork was called.
    void someSimpleStuff() {}
};


class C
{
public:
    C(D& d, int param) : d(d), m(param) { startWork(param); }

    // Uses m. Called multiple times.
    // Guaranteed to only be called after startWork was called
    void doProcessing() {}

private:
    // Called single time, once param is known and work can be started.
    void startWork(int param) {}

    D& d;
    M m;
};

int main()
{
    D d;
    d.someSimpleStuff();

    C c(d, 1337);
    c.doProcessing();
    c.doProcessing();
}

那并不能使C可构建。 - Danra
@Danra,我已经修改了答案。这是否是您问题的解决方案? - rozina
很遗憾,不行。因为我并没有直接调用C语言的方法——超出我控制范围的外部代码期望在一个单一类中使用接口,该接口包含了someSimpleStuff、startWork和doProcessing的实现。 - Danra
@Danra:你应该在问题中提供这个限制条件。询问解决方案,然后通过引入迄今未提及的限制条件来拒绝它们有点不合适。我会点赞这个答案,因为它是对所述问题的一个很好的回答。没有人能指望成为一个心灵感应者。 - Cheers and hth. - Alf
我的错,补充一下问题。你让这个遗漏听起来像是故意的,但实际上只是我在抽象化我的具体问题和提供足够细节之间平衡不当。 - Danra

0
问题是“是否可以在不分割C接口的情况下,在编译时检查m是否只在初始化后使用?”
答案是不行,您必须使用类型系统来确保对象M在初始化之前不被使用,这意味着要分割C接口。在编译时,编译器只知道对象的类型和常量表达式的值。C不能是一个字面类型。因此,您必须使用类型系统:您必须分割C接口以确保在编译时仅在初始化后使用M。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接