从构造函数抛出异常

发布于 2024-07-18 04:20:09 字数 805 浏览 10 评论 0原文

我正在与一位同事就构造函数抛出异常进行辩论,并认为我需要一些反馈。

从设计的角度来看,可以从构造函数中抛出异常吗?

假设我将 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 技术交流群。

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

发布评论

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

评论(11

﹏雨一样淡蓝的深情 2024-07-25 04:20:10

除了在特定情况下您不需要从构造函数中抛出的事实之外,因为 pthread_mutex_lock 实际上会返回一个 EINVAL 如果您的互斥体尚未初始化,并且您可以在调用 lock 后抛出异常,如下所示在 std::mutex 中完成:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

那么一般来说,对于构造过程中的获取错误,从构造函数抛出是可以的,并且符合 RAII(资源获取即初始化)编程范例。

检查这个RAII 示例

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

重点关注这些语句:

  1. static std::mutex mutex <
  2. 代码>std::lock_guard; lock(mutex);
  3. 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_ 对象通过 lockedunlocked 之间发生变化。 code>Mutex 公共方法 Mutex::lock()Mutex::unlock()

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};

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 to lock as is done in std::mutex:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

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

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

Focus on these statements:

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

The first statement is RAII and noexcept. In (2) it is clear that RAII is applied on lock_guard and it actually can throw , whereas in (3) ofstream seems not to be RAII , since the objects state has to be checked by calling is_open() that checks the failbit 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 from std::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.

  • Resource : your object
  • Acquisition ( allocation ) : you object being created
  • Initialization : your object is in its invariant state

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 the mutex_ object mutates between locked and unlocked through the Mutex public methods Mutex::lock() and Mutex::unlock().

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};
萌︼了一个春 2024-07-25 04:20:10

请注意,在构造函数抛出异常后,析构函数永远不会被调用

struct B
{
    char* p;
    B() { 
        cout << "Constructor - B" << endl; 
        p = new char[1024];
        throw std::exception("some exception");
    }
    ~B() { // NEVER GETS CALLED AFTER EXCEPTION !!!! - memory leak 
        cout << "Destructor - B" << endl; 
        delete[] p;
    } 
};

int main()
{
    try {
        B b;
    }
    catch (...) {
        cout << "Catch called " << endl;
    }
}

输出:

Constructor - B
Catch called       (Note: B's Destructor is NEVER called)

Be aware that the destructor never gets called after the exception is thrown from the constructor.

struct B
{
    char* p;
    B() { 
        cout << "Constructor - B" << endl; 
        p = new char[1024];
        throw std::exception("some exception");
    }
    ~B() { // NEVER GETS CALLED AFTER EXCEPTION !!!! - memory leak 
        cout << "Destructor - B" << endl; 
        delete[] p;
    } 
};

int main()
{
    try {
        B b;
    }
    catch (...) {
        cout << "Catch called " << endl;
    }
}

Output:

Constructor - B
Catch called       (Note: B's Destructor is NEVER called)
萌︼了一个春 2024-07-25 04:20:10

虽然我没有达到专业水平的 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.

橘和柠 2024-07-25 04:20:09

是的,从失败的构造函数中抛出异常是执行此操作的标准方法。 请阅读有关处理失败的构造函数的常见问题解答,了解更多信息。 拥有 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.

北凤男飞 2024-07-25 04:20:09

如果确实从构造函数抛出异常,请记住,如果需要在构造函数初始值设定项列表中捕获该异常,则需要使用函数 try/catch 语法。

例如

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }

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.

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

vs.

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }
梅窗月明清似水 2024-07-25 04:20:09

抛出异常是处理构造函数失败的最佳方法。 您应该特别避免半构造一个对象,然后依赖类的用户通过测试某种标志变量来检测构造失败。

与此相关的是,您有几种不同的异常类型来处理互斥错误,这一事实让我有点担心。 继承是一个很好的工具,但它可能会被过度使用。 在这种情况下,我可能更喜欢一个 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.

梦萦几度 2024-07-25 04:20:09
#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}

输出:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

析构函数没有被调用,所以如果需要在构造函数中抛出异常,需要做很多事情(例如清理?)。

#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}

the output:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

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.

山人契 2024-07-25 04:20:09

从你的构造函数中抛出是可以的,但你应该确保
你的对象是在 main 启动之后和之前构建的
完成:

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}

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:

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}
葬心 2024-07-25 04:20:09

唯一不会从构造函数抛出异常的情况是,如果您的项目有禁止使用异常的规则(例如,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.

回心转意 2024-07-25 04:20:09

添加到这里的所有答案中,我想提一下,一个非常具体的原因/场景,您可能希望更喜欢从类的 Init 方法而不是从 Ctor 抛出异常(当然是首选且更常见的方法)。

我将提前提到,此示例(场景)假设您的类不使用“智能指针”(即 std::unique_ptr)。
s 指针数据成员。

言归正传:如果您希望类的 Dtor 在您捕获 Init() 的异常之后(对于本例)调用它时“采取行动” 方法抛出 - 你不能从 Ctor 抛出异常,因为 Ctor 的 Dtor 调用不会在“半生不熟”的对象上调用。

请参阅下面的示例来证明我的观点:

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}

我会再次提到,这不是推荐的方法,只是想分享一个额外的观点。

另外,正如您可能从代码中的一些打印中看到的那样 - 它基于 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:

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}

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).

格子衫的從容 2024-07-25 04:20:09

如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数抛出异常是比不抛出更好的解决方案。 如果没有抛出异常,则对象将以僵尸状态初始化。 这样的对象需要公开一个标志来表明该对象是否正确。 像这样的事情:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

这种方法的问题在于调用方。 该类的每个用户在实际使用该对象之前都必须执行 if 操作。 这是对错误的呼吁 - 没有什么比在继续之前忘记测试条件更简单的了。

如果构造函数抛出异常,构造对象的实体应该立即处理问题。 下游的对象消费者可以根据他们获得对象的事实,随意假设该对象是 100% 可操作的。

这种讨论可以在很多方面继续下去。

例如,使用异常作为验证是一种不好的做法。 一种方法是将 Try 模式与工厂类结合使用。 如果您已经在使用工厂,那么编写两个方法:

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

使用此解决方案,您可以就地获取状态标志,作为工厂方法的返回值,而无需输入包含错误数据的构造函数。

第二件事是您是否用自动化测试覆盖代码。 在这种情况下,使用不抛出异常的对象的每一段代码都必须进行一项额外的测试 - 当 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:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

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:

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文