基本上,这一切都归结为从以下三种设计中选择:
设计
免责声明:这篇文章不鼓励使用例外规范或例外。如果您愿意,可以使用错误代码等效地报告错误。此处使用的异常规范只是为了说明何时使用简洁的语法会发生不同的错误。
设计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);
};
优点:
- 用户可以控制何时调用“繁重”初始化过程
- 可以在密钥存在之前创建对象。这对于 COM 等框架很重要,其中所有对象必须 具有默认构造函数(
CoCreateObject() 不允许您将额外参数转发给对象构造函数)。有时,仍然有解决方法,例如 builder 对象。
缺点:
- 在使用对象之前,必须检查对象的陈旧状态。这可能由对象通过返回错误代码或抛出异常来强制执行。就个人而言,我讨厌允许我使用它们并且似乎忽略我的调用的对象(例如失败的
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。
设计 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 是不可能的,因为它只会导致软件中出现更多错误。
问题只是关于何时支付初始化成本。您喜欢先付款还是首次使用?在大多数情况下,我更喜欢预付费用,因为我不想假设用户可以在程序后期处理错误。但是,您的程序中可能存在特定的线程语义,并且您可能无法在创建时(或者相反,在使用时)停止线程。
无论如何,您仍然可以通过在设计 #2 中使用类的动态分配来获得设计 #3 的好处。
结论
基本上,如果您犹豫的唯一原因是构造函数快速执行的某种哲学理想,我会选择纯粹的 RAII 设计。