【问题标题】:Best practice for deferred initialization of private class members延迟初始化私有类成员的最佳实践
【发布时间】:2017-09-05 15:10:11
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 通过使用 uniqe_ptr 和 get() 函数的包装类,您可以确保在未初始化的情况下不使用它。但是,您无法确保您的代码不会尝试这样做,因此您仍然受限于运行时错误或默认参数。
  • 为什么需要使用延迟初始化?你打算什么时候做? m 甚至需要成为会员吗?从您的示例代码看来,每次您想在C 中做一些“工作”时,您都在传递param
  • @SimonKraemer 澄清代码示例。
  • 这可能(或可能不)在这里相关:stackoverflow.com/questions/35338724/…

标签: c++ constructor


【解决方案1】:

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

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

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

【讨论】:

  • @Danara 您是否需要M 功能之外的startWork()?如果你不这样做,那么 M 本地到 startWork() 并且你可以在每次调用 startWork() 时创建它。
  • 不幸的是,这并不总是可能的。例如该参数实际上仅在调用 startWork 时才知道,但该对象之前确实存在,并且确实具有一些有限的公共功能。可以将m 重构为C 的内部类D,并让该内部类具有非默认构造函数——但这并不能解决问题,它只是将其代理给D 成员C.
  • @rozina 是的,我愿意。修改示例以强调这一事实。
  • 在这种情况下,额外的功能可以封装在另一个类中,该类在构造时接收已经存在的对象。
  • @Danra 第二次调用 startWork() 会发生什么?你会用M做什么?你可以创建一个新的并扔掉旧的吗?
【解决方案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 类。

【讨论】:

  • 线程安全呢?我怀疑这种设计是线程安全的。
  • @KingThrushbeard 我已对其进行了修改并添加了(可选)线程安全性。
【解决方案3】:

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

if(m)
    m->foo();

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

【讨论】:

  • 这是明显的缺点。但是,如果您需要延迟初始化,我仍然认为这是最好的方法。但是,您不能一直避免这种情况。
  • 是的,但是人们在方法中添加“只是一个 if”,并没有意识到他们已经引入了一个状态并且需要在每一位代码中管理它。但是当然,当延迟初始化时,这是不可避免的。只是为了强调这一点。无论如何+1 ;)
【解决方案4】:

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

您将不需要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();
}

【讨论】:

  • 这不会使 C 可构造。
  • @Danra 我已经改变了答案。这可以解决您的问题吗?
  • 不幸的是,不,因为我不是真正调用 C 方法的人 - 超出我控制的外部代码是期望在单个类中实现一些 SimpleStuff、startWork 和 doProcessing 的接口。
  • @Danra:您应该在问题中提供该约束。寻求解决方案,然后通过引入迄今为止未提及的约束来消除它们,这有点反社会。我赞成这一点,因为它是对所述问题的一个很好的回答。没有人可以期望成为心灵感应者。
  • @Cheersandhth.-Alf 我的错,补充问题。您确实故意省略了这个,这只是我在抽象我的具体问题和提供足够详细信息之间的不正确平衡。
【解决方案5】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-20
    • 1970-01-01
    • 2022-12-28
    • 2010-09-22
    • 2013-02-20
    • 1970-01-01
    • 2010-12-30
    相关资源
    最近更新 更多