从构造函数抛出异常
我正在与一位同事就构造函数抛出异常进行辩论,并认为我需要一些反馈。
从设计的角度来看,可以从构造函数中抛出异常吗?
假设我将 POSIX 互斥体包装在一个类中,它看起来像这样:
class Mutex {
public:
Mutex() {
if (pthread_mutex_init(&mutex_, 0) != 0) {
throw MutexInitException();
}
}
~Mutex() {
pthread_mutex_destroy(&mutex_);
}
void lock() {
if (pthread_mutex_lock(&mutex_) != 0) {
throw MutexLockException();
}
}
void unlock() {
if (pthread_mutex_unlock(&mutex_) != 0) {
throw MutexUnlockException();
}
}
private:
pthread_mutex_t mutex_;
};
我的问题是,这是执行此操作的标准方法吗? 因为如果 pthread mutex_init 调用失败,则互斥体对象将不可用,因此抛出异常可确保不会创建互斥体。
我是否应该为 Mutex 类创建一个成员函数 init 并调用 pthread mutex_init ,其中将根据 pthread mutex_init 的返回返回一个 bool ? 这样我就不必对如此低级别的对象使用异常。
I'm having a debate with a co-worker about throwing exceptions from constructors, and thought I would like some feedback.
Is it OK to throw exceptions from constructors, from a design point of view?
Lets say I'm wrapping a POSIX mutex in a class, it would look something like this:
class Mutex {
public:
Mutex() {
if (pthread_mutex_init(&mutex_, 0) != 0) {
throw MutexInitException();
}
}
~Mutex() {
pthread_mutex_destroy(&mutex_);
}
void lock() {
if (pthread_mutex_lock(&mutex_) != 0) {
throw MutexLockException();
}
}
void unlock() {
if (pthread_mutex_unlock(&mutex_) != 0) {
throw MutexUnlockException();
}
}
private:
pthread_mutex_t mutex_;
};
My question is, is this the standard way to do it? Because if the pthread mutex_init
call fails the mutex object is unusable so throwing an exception ensures that the mutex won't be created.
Should I rather create a member function init for the Mutex class and call pthread mutex_init
within which would return a bool based on pthread mutex_init
's return? This way I don't have to use exceptions for such a low level object.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
除了在特定情况下您不需要从构造函数中抛出的事实之外,因为
pthread_mutex_lock
实际上会返回一个 EINVAL 如果您的互斥体尚未初始化,并且您可以在调用lock
后抛出异常,如下所示在std::mutex
中完成:那么一般来说,对于构造过程中的获取错误,从构造函数抛出是可以的,并且符合 RAII(资源获取即初始化)编程范例。
检查这个RAII 示例
重点关注这些语句:
static std::mutex mutex <
std::ofstream file("example.txt");
第一个语句是 RAII 和
noexcept
。 在(2)中很明显,RAII应用于lock_guard
并且它实际上可以抛出
,而在(3)中ofstream
似乎不是RAII ,因为必须通过调用检查failbit
标志的is_open()
来检查对象状态。乍一看,它似乎还没有决定它的标准方式,并且在第一种情况下
std::mutex
不会抛出初始化,*与 OP 实现相反*。 在第二种情况下,它将抛出从 std::mutex::lock 抛出的任何内容,而在第三种情况下,根本不抛出任何异常。注意区别:
(1) 可以声明为静态,并且实际上会声明为成员变量
(2) 实际上永远不会期望被声明为成员变量
(3)期望被声明为成员变量,并且底层资源可能并不总是可用。
所有这些形式都是RAII; 要解决这个问题,必须分析RAII。
这不需要您在构造时初始化和连接所有内容。 例如,当您创建一个网络客户端对象时,您实际上不会在创建时将其连接到服务器,因为这是一个缓慢的操作,并且会失败。 您可以编写一个
connect
函数来完成此操作。 另一方面,您可以创建缓冲区或仅设置其状态。因此,您的问题归结为定义您的初始状态。 如果在您的情况下,您的初始状态是必须初始化互斥体,那么您应该从构造函数中抛出。 相反,最好不要初始化(如
std::mutex
中所做的那样),并将不变状态定义为 mutex is created 。 无论如何,不变量不一定会受到其成员对象的状态的影响,因为mutex_
对象通过locked
和unlocked
之间发生变化。 code>Mutex 公共方法Mutex::lock()
和Mutex::unlock()
。Apart from the fact that you do not need to throw from the constructor in your specific case because
pthread_mutex_lock
actually returns an EINVAL if your mutex has not been initialized and you can throw after the call tolock
as is done instd::mutex
:then in general throwing from constructors is ok for acquisition errors during construction, and in compliance with RAII ( Resource-acquisition-is-Initialization ) programming paradigm.
Check this example on RAII
Focus on these statements:
static std::mutex mutex
std::lock_guard<std::mutex> lock(mutex);
std::ofstream file("example.txt");
The first statement is RAII and
noexcept
. In (2) it is clear that RAII is applied onlock_guard
and it actually canthrow
, whereas in (3)ofstream
seems not to be RAII , since the objects state has to be checked by callingis_open()
that checks thefailbit
flag.At first glance it seems that it is undecided on what it the standard way and in the first case
std::mutex
does not throw in initialization , *in contrast to OP implementation * . In the second case it will throw whatever is thrown fromstd::mutex::lock
, and in the third there is no throw at all.Notice the differences:
(1) Can be declared static, and will actually be declared as a member variable
(2) Will never actually be expected to be declared as a member variable
(3) Is expected to be declared as a member variable, and the underlying resource may not always be available.
All these forms are RAII; to resolve this, one must analyse RAII.
This does not require you to initialize and connect everything on construction. For example when you would create a network client object you would not actually connect it to the server upon creation, since it is a slow operation with failures. You would instead write a
connect
function to do just that. On the other hand you could create the buffers or just set its state.Therefore, your issue boils down to defining your initial state. If in your case your initial state is mutex must be initialized then you should throw from the constructor. In contrast it is just fine not to initialize then ( as is done in
std::mutex
), and define your invariant state as mutex is created . At any rate the invariant is not compromized necessarily by the state of its member object, since themutex_
object mutates betweenlocked
andunlocked
through theMutex
public methodsMutex::lock()
andMutex::unlock()
.请注意,
在构造函数抛出异常后,析构函数永远不会被调用
。输出:
Be aware that
the destructor never gets called after the exception is thrown from the constructor
.Output:
虽然我没有达到专业水平的 C++ 工作经验,但在我看来,从构造函数中抛出异常是可以的。 我在 .Net 中这样做(如果需要)。 查看此和此链接。 这可能是你感兴趣的。
Although I have not worked C++ at a professional level, in my opinion, it is OK to throw exceptions from the constructors. I do that(if needed) in .Net. Check out this and this link. It might be of your interest.
是的,从失败的构造函数中抛出异常是执行此操作的标准方法。 请阅读有关处理失败的构造函数的常见问题解答,了解更多信息。 拥有 init() 方法也可以,但是创建互斥体对象的每个人都必须记住必须调用 init() 。 我觉得这违反了 RAII 原则。
Yes, throwing an exception from the failed constructor is the standard way of doing this. Read this FAQ about Handling a constructor that fails for more information. Having a init() method will also work, but everybody who creates the object of mutex has to remember that init() has to be called. I feel it goes against the RAII principle.
如果确实从构造函数抛出异常,请记住,如果需要在构造函数初始值设定项列表中捕获该异常,则需要使用函数 try/catch 语法。
例如
与
If you do throw an exception from a constructor, keep in mind that you need to use the function try/catch syntax if you need to catch that exception in a constructor initializer list.
e.g.
vs.
抛出异常是处理构造函数失败的最佳方法。 您应该特别避免半构造一个对象,然后依赖类的用户通过测试某种标志变量来检测构造失败。
与此相关的是,您有几种不同的异常类型来处理互斥错误,这一事实让我有点担心。 继承是一个很好的工具,但它可能会被过度使用。 在这种情况下,我可能更喜欢一个 MutexError 异常,可能包含一条信息丰富的错误消息。
Throwing an exception is the best way of dealing with constructor failure. You should particularly avoid half-constructing an object and then relying on users of your class to detect construction failure by testing flag variables of some sort.
On a related point, the fact that you have several different exception types for dealing with mutex errors worries me slightly. Inheritance is a great tool, but it can be over-used. In this case I would probably prefer a single MutexError exception, possibly containing an informative error message.
输出:
析构函数没有被调用,所以如果需要在构造函数中抛出异常,需要做很多事情(例如清理?)。
the output:
the destructors are not called, so if a exception need to be thrown in a constructor, a lot of stuff(e.g. clean up?) to do.
从你的构造函数中抛出是可以的,但你应该确保
你的对象是在 main 启动之后和之前构建的
完成:
It is OK to throw from your constructor, but you should make sure that
your object is constructed after main has started and before it
finishes:
唯一不会从构造函数抛出异常的情况是,如果您的项目有禁止使用异常的规则(例如,Google 不喜欢例外)。 在这种情况下,您不会希望在构造函数中比其他任何地方更多地使用异常,并且您必须有某种 init 方法。
The only time you would NOT throw exceptions from constructors is if your project has a rule against using exceptions (for instance, Google doesn't like exceptions). In that case, you wouldn't want to use exceptions in your constructor any more than anywhere else, and you'd have to have an init method of some sort instead.
添加到这里的所有答案中,我想提一下,一个非常具体的原因/场景,您可能希望更喜欢从类的
Init
方法而不是从 Ctor 抛出异常(当然是首选且更常见的方法)。我将提前提到,此示例(场景)假设您的类不使用“智能指针”(即
std::unique_ptr
)。s 指针数据成员。
言归正传:如果您希望类的 Dtor 在您捕获
Init() 的异常之后(对于本例)调用它时“采取行动”
方法抛出 - 你不能从 Ctor 抛出异常,因为 Ctor 的 Dtor 调用不会在“半生不熟”的对象上调用。请参阅下面的示例来证明我的观点:
我会再次提到,这不是推荐的方法,只是想分享一个额外的观点。
另外,正如您可能从代码中的一些打印中看到的那样 - 它基于 Scott Meyers 撰写的精彩的“更有效的 C++”(第一版)中的第 10 项。
Adding to all the answers here, I thought to mention, a very specific reason/scenario where you might want to prefer to throw the exception from the class's
Init
method and not from the Ctor (which off course is the preferred and more common approach).I will mention in advance that this example (scenario) assumes that you don't use "smart pointers" (i.e.-
std::unique_ptr
) for your class's pointer(s) data members.
So to the point: In case, you wish that the Dtor of your class will "take action" when you invoke it after (for this case) you catch the exception that your
Init()
method threw - you MUST not throw the exception from the Ctor, cause a Dtor invocation for Ctor's are NOT invoked on "half-baked" objects.See the below example to demonstrate my point:
I will mention again, that it is not the recommended approach, just wanted to share an additional point of view.
Also, as you might have seen from some of the print in the code - it is based on item 10 in the fantastic "More effective C++" by Scott Meyers (1st edition).
如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数抛出异常是比不抛出更好的解决方案。 如果没有抛出异常,则对象将以僵尸状态初始化。 这样的对象需要公开一个标志来表明该对象是否正确。 像这样的事情:
这种方法的问题在于调用方。 该类的每个用户在实际使用该对象之前都必须执行 if 操作。 这是对错误的呼吁 - 没有什么比在继续之前忘记测试条件更简单的了。
如果构造函数抛出异常,构造对象的实体应该立即处理问题。 下游的对象消费者可以根据他们获得对象的事实,随意假设该对象是 100% 可操作的。
这种讨论可以在很多方面继续下去。
例如,使用异常作为验证是一种不好的做法。 一种方法是将 Try 模式与工厂类结合使用。 如果您已经在使用工厂,那么编写两个方法:
使用此解决方案,您可以就地获取状态标志,作为工厂方法的返回值,而无需输入包含错误数据的构造函数。
第二件事是您是否用自动化测试覆盖代码。 在这种情况下,使用不抛出异常的对象的每一段代码都必须进行一项额外的测试 - 当 IsValid() 方法返回 false 时它是否正确运行。 这很好地解释了在僵尸状态下初始化对象是一个坏主意。
If your project generally relies on exceptions to distinguish bad data from good data, then throwing an exception from the constructor is better solution than not throwing. If exception is not thrown, then object is initialized in a zombie state. Such object needs to expose a flag which says whether the object is correct or not. Something like this:
Problem with this approach is on the caller side. Every user of the class would have to do an if before actually using the object. This is a call for bugs - there's nothing simpler than forgetting to test a condition before continuing.
In case of throwing an exception from the constructor, entity which constructs the object is supposed to take care of problems immediately. Object consumers down the stream are free to assume that object is 100% operational from the mere fact that they obtained it.
This discussion can continue in many directions.
For example, using exceptions as a matter of validation is a bad practice. One way to do it is a Try pattern in conjunction with factory class. If you're already using factories, then write two methods:
With this solution you can obtain the status flag in-place, as a return value of the factory method, without ever entering the constructor with bad data.
Second thing is if you are covering the code with automated tests. In that case every piece of code which uses object which does not throw exceptions would have to be covered with one additional test - whether it acts correctly when IsValid() method returns false. This explains quite well that initializing objects in zombie state is a bad idea.