我的类的构造函数应该执行多少工作?

发布于 2024-12-28 05:27:48 字数 541 浏览 2 评论 0原文

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

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

令我困扰的是,现在构造函数中正在完成大量工作。 在可以安全地使用对象的 I/O 例程之前,首先需要初始化编解码器(这不是要求很高),然后考虑密钥并初始化加密和其他内容 - 这些需要对以下内容进行一些分析需要大量计算的媒体。

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

你认为什么是好的设计?

PS我确实读过类似的问题,但我无法真正将答案应用于此案例。他们大多是“这取决于”... :-/

谢谢

I have a class that represents a data stream, it basically
reads or writes into a file, but first the data are being encrypted/decrypted and there is also an underlying codec object that handles the media being accessed.

I'm trying to write this class in a RAII way and I'd like a clean, nice, usable design.

What bothers me is that right now there is a lot of work being done in the constructor.
Before the object's I/O routines can be safely used, first of all the codec needs to initialized (this isn't very demanding), but then a key is taken into account and crypto and other things are intialized - these require some analysis of the media which takes quite a lot of computation.

Right now I'm doing all this in the constructor, which makes it take a long time. I'm thinking of moving the crypto init stuff (most work) out of the ctor into a separate method (say, Stream::auth(key)), but then again, this would move some responsibility to the user of the class, as they'd be required to run auth() before they call any I/O ops. This also means I'd have to place a check in the I/O calls to verify that auth() had been called.

What do you think is a good design?

P.S. I did read similar question but I wasn't really able to apply the answers on this case. They're mostly like "It depens"... :-/

Thanks

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

故事未完 2025-01-04 05:27:48

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

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

。 ,最好让构造函数构造您的类。通常,在类真正准备好使用之前,您不会认为它是完全“构造”的,但例外情况确实存在。
然而,请记住,在 RAII 中,关键思想之一是类不应该存在,除非它已经准备好、初始化并且可用。这就是为什么它的析构函数会进行清理,这就是为什么它的构造函数应该进行设置。

同样,例外确实存在(例如,某些 RAII 对象允许您释放资源并尽早执行清理,然后让析构函数不执行任何操作。)
因此,归根结底,这取决于您自己的判断。

从不变量的角度来思考它。如果我获得了你们班级的一个实例,我可以依赖什么?我对它的假设越多,它就越容易使用。如果它可能可以使用,并且可能处于某种“已构造,但未初始化”状态,并且可能处于“已清理”状态已启动但未销毁”状态,那么使用它很快就会变得痛苦。

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

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

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

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

The only truly golden unbreakable rule is that the class must be in a valid, consistent, state after the constructor has executed.

You can choose to design the class so that it is in some kind of "empty"/"inactive" state after the constructor has run, or you can put it directly into the "active" state that it is intended to be in.

Generally, it should be preferred to have the constructor construct your class. Usually, you wouldn't consider a class fully "constructed", until it's actually ready to be used, but exceptions do exist.
However, keep in mind that in RAII, one of the key ideas is that the class shouldn't exist unless it is ready, initalized and usable. That's why its destructor does the cleanup, and that's why its constructor should do the setup.

Again, exceptions do exist (for example, some RAII objects allow you to release the resource and perform cleanup early, and then have the destructor do nothing.)
So at the end of the day, it depends, and you'll have to use your own judgment.

Think of it in terms of invariants. What can I rely on if I'm given an instance of your class? The more I can safely assume about it, the easier it is to use. If it might be ready to use, and might be in some "constructed, but not initialized" state, and might be in a "cleaned up but not destroyed" state, then using it quickly becomes painful.

On the other hand, if it guarantees that "if the object exists, it can be used as-is", then I'll know that I can use it without worrying about what was done to it before.

It sounds like your problem is that you're doing too much in the constructor.

What if you split the work up into multiple smaller classes? Have the codec be initialized separately, then I can simply pass the already-initialized codec to your constructor. And all the authentication and cryptography stuff and whatnot could possibly be moved out into separate objects as well, and then simply passed to "this" constructor once they're ready.

Then the remaining constructor doesn't have to do everything from scratch, but can start from a handful of helper objects which are already initialized and ready to be used, so it just has to connect the dots.

九歌凝 2025-01-04 05:27:48

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

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

you could just place the check in the IO calls to see if auth has been called, and if it has, then continue, if not, then call it.

this removes the burden from the user, and delays the expense until needed.

爺獨霸怡葒院 2025-01-04 05:27:48

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

设计

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


设计 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 设计。

Basically, this all boils down to which design to choose from the following three:

Designs

Disclaimer: this post is not encouraging the use of exception specifications or exceptions for that matter. The errors may equivalently be reported using error codes if you wish. Exception specifications as used here are just meant to illustrate when different errors can occur using a concise syntax.


Design 1

This is the most recurring design out there, and totally non-RAII. The constructor just puts the object in some stale state and each instance must be initialized manually after construction takes place.

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

Pros:

  1. Users have control over when to invoke the "heavy" initialization process
  2. The object can be created before the key exists. This is important for frameworks such as COM, where all objects must have a default constructor (the CoCreateObject() does not allow you to forward extra arguments the object constructor). Sometimes, there are still workarounds, such as a builder object.

Cons:

  1. Objects must be checked for the stale state before using the object. This may be enforced by the object by returning an error code or throwing an exception. Personally, I hate objects that allow me to use them and just appear to ignore my calls (e.g. a failed std::ostream).

Design 2

This is the RAII approch. Make sure the object is 100% usable with no extra artefacts (e.g. manually calling stream.initialize(...); on each instance.

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

Pros:

  1. The object can always be assumed to be in a valid state. This is so much simpler to use.

Cons:

  1. Constructor might take a long time to execute.
  2. All required arguments must be available at the instance construction. This has once in a while been a problem for me, especially if most other objects in the code base use design #1.

Design 3

Somewhat of a compromise between the two previous cases. Don't initialize yet, but have the other methods lazily invoke the internal .initialize(...) method when necessary.

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

Pros:

  1. Almost as easy to use as design #1. Almost (see below).

Cons:

  1. If the initialization step may fail, it may now fail anywhere there is a first call to any of the public methods. Proper error handling for this scenario is extremely difficult.

Discussion

If you absolutely must pay for the initialization for every instance, then design #1 is out of the question as it just results in more bugs in the software.

The question is just about when to pay for the initialization cost. Do you prefer paying it upfront, or on first use? In most scenarios, I prefer paying upfront because I don't want to assume users can handle errors later in the program. However, there might be specific threading semantics in your program, and you might not be able to stall threads at creation time (or, conversely, at use time).

In any case, you can still get the benefits of design #3 by using dynamic allocation of the class in design #2.

Conclusion

Basically, if the only reason you are hesitating is for some philosophical ideal where constructors execute quickly, I would just go with the pure RAII design.

¢蛋碎的人ぎ生 2025-01-04 05:27:48

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

  • 创建初始化程序列表的对象的顺序可能会引起微妙的
  • 错误构造函数中的异常?您需要在应用程序中处理部分构造的对象吗?

There's no hard and fast rule on this, but in general it's best to avoid heavy constructors for two reasons that come to mind (maybe others as well):

  • The order of the objects created intializer list can give rise to subtle bugs
  • What to do with exceptions in the constructor? Will you need to handle partially-constructed objects in your app?
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文