私有类成员延迟初始化的最佳做法

Best practice for deferred initialization of private class members

本文关键字:最佳 初始化 延迟 成员      更新时间:2023-10-16

是否有延迟初始化私有类成员MC的最佳实践?例如:

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);
};

无法构造类 C 的对象,因为M没有默认的初始值设定项。

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

如果没有,你可以将C的成员m包装在std::unique_ptr中,并在可能的情况下构造它。

但是,这两种解决方案都容易出现在运行时会捕获的错误。是否有一些做法可以在编译时确保m仅在初始化后使用?

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

最佳做法是永远不要使用延迟初始化。

在您的情况下,放弃 C 的默认构造函数并将其替换为 C(int param) : m(param){} 。也就是说,类成员在构造点使用基本成员初始化进行初始化。

使用延迟初始化意味着您的对象可能处于未定义状态,并且实现并发等目标更加困难。

#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 类。

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

if(m)
    m->foo();

我知道这不是编译时检查,但据我所知,当前的编译器无法进行检查。代码分析必须非常复杂才能看到这样的东西,因为您可以随时以任何方法初始化m,或者 - 如果公共/受保护 - 甚至在另一个文件中。编译时检查意味着延迟初始化是在编译时完成的,但延迟初始化的概念是基于运行时的。

好的,根据我对你的问题的了解,这是一个解决方案吗?

您将不需要M的功能放入class 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();
}

问题是"是否可以在编译时检查 m 是否仅在初始化后才使用而不拆分 C 的接口?

答案是否定的,您必须使用类型系统来确保在初始化之前不使用对象 M,这意味着拆分 C 接口。在编译时,编译器只知道对象的类型和常量表达式的值。C 不能是文本类型。所以你必须使用类型系统:你必须拆分 C 接口,以确保在编译时 M 只在初始化后使用。