【问题标题】:How much work should constructor of my class perform?我的班级的构造函数应该执行多少工作?
【发布时间】:2012-01-18 19:10:42
【问题描述】:

我有一个代表数据流的类,它基本上 读取或写入文件,但首先要对数据进行加密/解密,并且还有一个底层编解码器对象来处理正在访问的媒体。

我正在尝试以 RAII 方式编写这个类,我想要一个干净、漂亮、可用的设计。

让我困扰的是,现在构造函数中有很多工作要做。 在可以安全使用对象的 I/O 例程之前,首先需要初始化编解码器(这不是很苛刻),但随后会考虑到密钥并初始化加密和其他东西 - 这些需要一些分析需要大量计算的媒体。

现在我在构造函数中做这一切,这使得它需要很长时间。我正在考虑将加密初始化的东西(大多数工作)从 ctor 中移出到一个单独的方法中(例如,Stream::auth(key)),但是话又说回来,这会将一些责任转移到类的用户身上,因为他们会需要运行auth()他们调用任何 I/O 操作。这也意味着我必须检查 I/O 调用以验证 auth() 是否已被调用。

你认为什么是好的设计?

附:我确实读过类似的问题,但我真的无法在这个案例中应用答案。他们大多像“它依赖”...... :-/

谢谢

【问题讨论】:

  • 我不知道你为什么首先认为长构造函数是一件坏事......?如果有很多初始化,构造函数会很长,句号。
  • 您提到您不喜欢构造函数中的这种初始化,因为它需要时间。把它放在一个单独的函数中如何减少这个时间?这些对象是否经常在不使用的情况下构建和销毁?
  • 唯一正确的答案“视情况而定”。 :)

标签: c++ raii


【解决方案1】:

唯一真正牢不可破的黄金法则是在构造函数执行后,类必须处于有效、一致的状态

您可以选择将类设计为在构造函数运行后处于某种“空”/“非活动”状态,或者您可以直接将其置于预期的“活动”状态在。

一般来说,最好让构造函数来构造你的类。通常,在真正准备好使用之前,您不会认为一个类是完全“构造的”,但确实存在异常。 但是,请记住,在 RAII 中,关键思想之一是类不应存在,除非它已准备好、已初始化且可用。这就是它的析构函数进行清理的原因,也是它的构造函数应该进行设置的原因。

同样,异常确实存在(例如,某些 RAII 对象允许您释放资源并提前执行清理,然后让析构函数不执行任何操作。) 所以说到底,这取决于你自己的判断。

从不变量的角度来考虑它。如果给我一个你的类的实例,我可以依靠什么?我越能放心地假设它,它就越容易使用。如果它可能准备好使用,并且可能处于某种“已构建,但未初始化”状态,并且可能处于“已清理up but not destroy" 状态,然后使用它很快就会变得痛苦。

另一方面,如果它保证“如果对象存在,则可以按原样使用”,那么我就知道我可以使用它,而不必担心之前对它做了什么。

听起来你的问题是你在构造函数中做的太多了。

如果您将工作分成多个较小的类怎么办?让编解码器单独初始化,然后我可以简单地将已经初始化的编解码器传递给您的构造函数。并且所有的身份验证和密码学的东西和诸如此类的东西也可能被移出到单独的对象中,然后在它们准备好后简单地传递给“this”构造函数。

那么剩下的构造函数就不必从头开始做所有事情,而是可以从少数已经初始化并准备好使用的辅助对象开始,所以它只需要连接点。

【讨论】:

  • 好的,我决定我很可能会提前初始化编解码器并预先分析媒体,然后将其传递给流的 ctor。我发现这实际上不会改变我的 API,API 基本上已经为此做好了准备;-)(PS 我也会保留旧的构造函数,在某些情况下它可能更合适)谢谢你的回答!
  • +1 非常非常信息丰富且切中要害。但是有很多但是,但除此之外,真正的黄金。但是删除一些但是。 :)
【解决方案2】:

您可以在 IO 调用中检查是否已调用 auth,如果已调用,则继续,如果没有,则调用它。

这减轻了用户的负担,并将费用推迟到需要时。

【讨论】:

  • 我不是 c++ 程序员,但我通常会尝试在任何 OO 应用程序的构造函数中放入最低限度的内容。我喜欢尽可能懒惰地做所有其他事情,所以在调用需要它的方法之前。我在大型系统中的许多方法可能具有检查属性是否已定义的代码,如果是则返回它,如果没有则计算它。我使用某种吸气剂来促进这一点。如果语言不支持 getter,我会创建名称以 get 为前缀的方法
  • 恐怕我做不到,因为身份验证步骤需要密钥。不过,我可能会让密钥传递给构造函数......
  • 懒惰地做所有事情使得几乎不可能预测行为。当我尝试使用该类时,如果在某些未知的其他调用期间发生在某个不可预测的时间,则处理身份验证错误要困难得多。如果我可以通过auth() 函数或构造函数显式执行此操作,那么我知道何时何地处理身份验证错误,并且在使用该类时我不必担心错误。
  • @jalf:我完全同意这个担忧。我已经提交了一个更明确地讨论这一点的答案。
【解决方案3】:

基本上,这一切都归结为从以下三种设计中选择:

设计

免责声明:这篇文章不鼓励使用例外规范或例外。如果您愿意,可以使用错误代码等效地报告错误。此处使用的异常规范只是为了说明何时使用简洁的语法会发生不同的错误。


设计1

这是最常见的设计,完全非 RAII。构造函数只是将对象置于某种陈旧状态,并且每个实例都必须在构造发生后手动初始化。

class SecureStream
{
public:
    SecureStream();
    void initialize(Stream&,const Key&) throw(InvalidKey,AlreadyInitialized);
    std::size_t get(      void*,std::size_t) throw(NotInitialized,IOError);
    std::size_t put(const void*,std::size_t) throw(NotInitialized,IOError);
};

优点

  1. 用户可以控制何时调用“繁重”初始化过程
  2. 可以在密钥存在之前创建对象。这对于 COM 等框架很重要,其中所有对象必须 具有默认构造函数(CoCreateObject() 不允许您将额外参数转发给对象构造函数)。有时,仍然有解决方法,例如 builder 对象。

缺点

  1. 在使用对象之前,必须检查对象的陈旧状态。这可能由对象通过返回错误代码或抛出异常来强制执行。就个人而言,我讨厌允许我使用它们并且似乎忽略我的调用的对象(例如失败的std::ostream)。

设计 2

这是 RAII 方法。确保对象 100% 可用,没有额外的人工制品(例如,在每个实例上手动调用 stream.initialize(...);

class SecureStream
{
public:
    SecureStream(Stream&,const Key&) throw(InvalidKey);
    std::size_t get(      void*,std::size_t) throw(IOError);
    std::size_t put(const void*,std::size_t) throw(IOError);
};

优点

  1. 可以始终假定对象处于有效状态。这使用起来非常简单。

缺点

  1. 构造函数可能需要很长时间才能执行。
  2. 所有必需的参数必须在实例构造中可用。这有时对我来说是个问题,特别是如果代码库中的大多数其他对象都使用设计#1。

设计 3

在前两种情况之间有所妥协。暂时不要初始化,但让其他方法在必要时懒惰地调用内部 .initialize(...) 方法。

class SecureStream
{
public:
    SecureStream(Stream&,const Key&);
    std::size_t get(      void*,std::size_t) throw(InvalidKey,IOError);
    std::size_t put(const void*,std::size_t) throw(InvalidKey,IOError);
private:
    void initialize() throw(InvalidKey);
};

优点

  1. 几乎与设计#1 一样易于使用。 几乎(见下文)。

缺点

  1. 如果初始化步骤可能失败,它现在可能会失败任何第一次调用任何公共方法的地方。在这种情况下正确处理错误非常困难。

讨论

如果您绝对必须为每个实例的初始化付费,那么设计#1 是不可能的,因为它只会导致软件中出现更多错误。

问题只是关于何时支付初始化成本。您喜欢先付款还是首次使用?在大多数情况下,我更喜欢预付费用,因为我不想假设用户可以在程序后期处理错误。但是,您的程序中可能存在特定的线程语义,并且您可能无法在创建时(或者相反,在使用时)停止线程。

无论如何,您仍然可以通过在设计 #2 中使用类的动态分配来获得设计 #3 的好处。

结论

基本上,如果您犹豫的唯一原因是构造函数快速执行的某种哲学理想,我会选择纯粹的 RAII 设计。

【讨论】:

    【解决方案4】:

    对此没有硬性规定,但一般来说最好避免使用重型构造函数,原因有两个(可能还有其他原因):

    • 创建的初始化程序列表中的对象顺序可能会导致细微的错误
    • 如何处理构造函数中的异常?您是否需要在应用中处理部分构造的对象?

    【讨论】:

    • 对象初始化顺序与构造函数执行的工作量(执行时间)有何关系?此外,“部分构造”的对象绝不是异常的结果,只是未在构造函数中完全初始化对象的结果。
    • @Andre 不是,我只是在评论 OP 关于清洁设计的第二段。
    • 如果您只是评论,请考虑将此评论设为评论,而不是答案。
    猜你喜欢
    • 2010-09-22
    • 1970-01-01
    • 1970-01-01
    • 2021-06-27
    • 1970-01-01
    • 1970-01-01
    • 2011-04-04
    • 2010-10-06
    • 1970-01-01
    相关资源
    最近更新 更多