DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

SBOMs are essential to circumventing software supply chain attacks, and they provide visibility into various software components.

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Beyond Sequential: Why Rust's Threading Model Changed How I Think About Concurrent Programming
  • Mastering Thread-Local Variables in Java: Explanation and Issues
  • Unlocking Performance: Exploring Java 21 Virtual Threads [Video]

Trending

  • How to Troubleshoot Common Linux VPS Issues: CPU, Memory, Disk Usage
  • How We Broke the Monolith (and Kept Our Sanity): Lessons From Moving to Microservices
  • Why Traditional CI/CD Falls Short for Cloud Infrastructure
  • A Software Engineer’s Guide to Thrive in Gen AI Era: Master It or Fade Out
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. Some Useful Facts to Know Before Using C++ Exceptions

Some Useful Facts to Know Before Using C++ Exceptions

Exceptions can be tricky. Read on to get some tips and hints on how to handle exceptions in C++, and keep your software running smooth.

By 
Manoj Piyumal user avatar
Manoj Piyumal
·
Dec. 05, 17 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
26.6K Views

Join the DZone community and get the full member experience.

Join For Free

Exceptions provide many benefits over error codes for error handling. Some of these benefits are:

  • Exceptions cannot be silently ignored whereas checking the error code of a method can be ignored by the method caller.
  • Exceptions propagate automatically over method boundaries, but error codes do not.
  • Exception handling removes error handling and recovery from the main line of control flow that makes code more readable and maintainable.
  • Exceptions are the best way to report errors from constructors and operators.

Despite these benefits, most people still do not prefer to use exceptions due to its overhead. Depending on the implementation, this overhead comes in two forms: time overhead (increased execution time) and space overhead (increased executable size and memory consumption). From these two, most are concerned about time overhead. However, in a good C++ exception implementation, unless an exception is thrown, no run-time overhead is incurred [2]. The real issue with C++ exceptions is not in their execution performance, but how to use them correctly. Following are some useful facts to know in order to use C++ exceptions correctly.

1. Exceptions Should Not Be Thrown From Destructors

 Consider the following code:

try
{
  MyClass1 Obj1;
  Obj1.DosomeWork();
  ...
}
catch(std::exception & ex)
{
   //do error handling
}

If an exception is thrown from the MyClass1:: DosomeWork() method, before execution moves out from the try block, the destructor of obj1 needs to be called as obj1 is a properly constructed object. What if an exception is also thrown from the destructor of MyClass1? This exception occurred while another exception was active. If an exception is thrown while another exception is active, the C++ behavior is to call the terminate() method, which halts the execution of the application. Therefore, to avoid two exceptions being active at the same time, destructors must not throw exceptions.

2. Objects Thrown as Exceptions Are Always Copied

When an exception is thrown, a copy of the exception object always needs to be created as the original object goes out of the scope during the stack unwinding process. Therefore, the exception object’s copy constructor will always be called. C++ provides a copy constructor by default if we don’t provide one. But there may be cases where the default copy constructor doesn’t work; especially when the class members are pointers.  If we use such objects as exception objects we should make sure that our exception classes have proper copy constructors.  The most important thing about copying objects in C++ is that the copy constructor is always called based on the static type, not the dynamic type. Consider the following code [1].

class Widget { ... };

class SpecialWidget: public Widget { ... };

void passAndThrowWidget()
{
    SpecialWidget localSpecialWidget;
    ...
    Widget& rw = localSpecialWidget;    // rw refers to a SpecialWidget

    throw rw;         // this throws an exception of type Widget!
}

In the above code, the throw statement throws an object of type Widget,  rw's static type is Widget. This might not be the behavior that we want to execute.

3. Exceptions Should Be Caught by Reference

There are three ways to catch exceptions in the catch clause:

  1. Catch exception by value.   
  2. Catch exception as a pointer.  
  3. Catch exception by reference.

Catching exceptions by value is costly and it suffers from the slicing problem. It is costly because it needs to create two exception objects every time. When an exception is thrown, a copy of the exception object needs to be created no matter how we catch it, because when the stack unwinds the original object would go out the scope. If the exception was caught by value, another copy is made to pass it to the catch clause. Therefore, if the exception was caught by value, two exception objects are created, making the exception throwing process slower.

The slicing problem comes into effect when the catch clause is declared to catch a super class type and a derived class object is thrown as an exception. In that case, the catch clause only receives a copy of the super class object, which lacks the attributes of the original exception object. Therefore, catching exceptions by value must be avoided.

If exceptions were caught as pointers, the code would be as follows;

void doSomething()
{
  try
  {
     someFunction();               // might throw an exception*
  }
  catch (exception* ex)
  {                // catches the exception*;    
    ...            // no object is copied
  }
}

In order to catch the exception as a pointer, the pointer to the exception object should be thrown and the throwing party must ensure that the exception object will be kept alive even after the stack unwinding, as a result of the throw. Even though a copy of the exception object will be created, it is a copy of the pointer in this case. Therefore, other measures should be taken to keep the exception object alive after the throw. This can be achieved by passing the pointer to a global or a static object, or creating the exception object in the heap.

Nevertheless, the user who catches the exception has no idea about how the exception object was created, and thus they are uncertain about whether to delete the receiving pointer at the catch clause or not. Therefore, catching the exception as a pointer is sub-optimal. Furthermore, all exceptions thrown from standard functions are objects, not pointers.

Catching exceptions by reference doesn’t have any of these issues that are observable in 'catch as a pointer' or 'catch by value.' The user doesn’t need to worry about the deletion of the exception object. No additional exception object will be copied, as a reference to the original exception object is passed. Furthermore, pass by reference doesn’t have the slicing problem. Therefore, the exception should be caught by reference for proper and efficient operation.

4. Prevent Resource Leaks in Case of Exceptions

Consider the following code:

void SomeFunction()
{
    SimpleObject* pObj = new SimpleObject();
    pObj->DoSomeWork();//could throw exceptions
    delete pObj;
}

In this method new SimpleObject is created, then some work has been done through  SimpleObject::DoSomeWork() method and finally  the object is destroyed.  But what if Object::DoSomeWork() threw an exception? In that case we don’t get a chance to delete the pObj. This introduces a memory leak. This is a simple example to illustrate that exceptions could lead to resource leaks and of course this can be eliminated by putting a simple try catch block. But in real life this scenario could happen from points of code where we can’t figure out the leak at first glance. One remedy for this type of cases is to use auto pointers from standard library (std::auto_ptr) [1].

5. When throwing an exception, do not leave the object in an inconsistent state

Consider the following example code [4]:

template <class T>
class Stack
{
   unsigned nelems;
   int top;
   T* v;
 public:
   void push(T);
   T pop();

   Stack();
   ~Stack();
};

template <class T>
void Stack<T>::push(T element)
{
  top++;
  if( top == nelems-1 )
  {
    T* new_buffer = new (nothrow) T[nelems+=10];
    if( new_buffer == 0 )
      throw "out of memory";

    for(int i = 0; i < top; i++)
      new_buffer[i] = v[i];


    delete [ ] v;
    v = new_buffer;
  }
  v[top] = element;
}

If the exception “out of memory” was thrown, the Stack::push() method leaves the Stack object in an inconsistent state, because the top of the stack has been incremented without pushing any element. Of course, this code can be modified so that it won't happen. However, special care needs to be taken when throwing an exception so that the object which throws the exception will be in a valid state even after the exception.
Furthermore, this scenario happens frequently with mutexes or locks. In the following naïve implementation of   ThreadSafeQueue::Pushback() method, if an exception was thrown from the DoPushBack() method, _mutex will be kept locked, leaving the ThreadSafeQueue object in an inconsistent state. To overcome this situation, lock_guards can be used, as auto pointers are used to prevent memory leaks. It should be noted that lock_guard has only been available in standard libraries since C++11, yet one can easily implement a lock_guard class.

template <class T>
void ThreadSafeQueue::Pushback(T element)
{
   _mutex.Lock();
   DoPushBack(T);
   _mutex.Unlock();
}

6. Exception Specification Should Be Used Carefully [1]

If a method throws an exception not listed in its exception specification, that fault is detected at runtime, and the special function unexpected()is automatically invoked. The default behavior for unexpected is to call  terminate(), and the default behavior for terminate() is to call abort(). Therefore, the default behavior for a program with a violated exception specification is to halt. Consider the following code:

void f1();                  // might throw anything

void f2() throw(int);        //throws only int

void f2() throw(int)
{
  ...
  f1();                  // legal even though f1 might throw
                         // something besides an int
  ...
}

The above code is legal because this kind of situation might arise when integrating old code that lacks exception specification, with new code. But if f1() throws something other than int the program will terminate because f2() is not allowed throw anything other than int. It is a bit hard to stop this problem from arising, but there are many ways to handle this if it does. Reference 1: Item 14 details remedies for this problem.

7. Exceptions Should Be Re-Thrown With Re-Throw

There are two ways to propagate a caught exception to callers. Consider the following two code blocks: 

catch (Widget& w)                 // catch Widget exceptions
{
    ...                             // handle the exception    
    throw;                          // rethrow the exception so it continues to propagate
}
catch (Widget& w)                 // catch Widget exceptions
{
    ...                             // handle the exception
     throw w;                        // propagate a copy of the
}   

The only difference between these two blocks is that the first one re-throws the current exception, while the second one throws a new copy of the current exception. There are two problems with the second case. One is the performance cost because of the copy operation. The other thing is the slicing problem. If the exception object is a child type of Widget, only the Widget part of the exception object is re-thrown as the copy is always based on the static type.

8. Catch Clause for the Base Class Should Be Placed After the Catch Clause for the Derived Class

When an exception is thrown, catch clauses are matched in the order they appear in the code. An exception object’s type also matches with its super types in catch clauses, because child type is also a subset of super type. Therefore, when a child object’s exception is thrown, if a super type catch clause appears first in the code, that will be executed, even though the catch clause for the child type is there after the super type’s catch clause.

References:

[1] More Effective C++, by Scott Meyers, 1996.

[2] When and How to Use Exceptions, by Herb Sutter, 2004 (http://www.drdobbs.com/when-and-how-to-use-exceptions/184401836).

[3] Technical Report on C++ Performance, 2005 (http://www.stroustrup.com/performanceTR.pdf).

[4] Exception Handling: A False Sense of Security, by Tom Cargill (http://ptgmedia.pearsoncmg.com/images/020163371x/supplements/Exception_Handling_Article.html).

Threading Pointer (computer programming)

Opinions expressed by DZone contributors are their own.

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Beyond Sequential: Why Rust's Threading Model Changed How I Think About Concurrent Programming
  • Mastering Thread-Local Variables in Java: Explanation and Issues
  • Unlocking Performance: Exploring Java 21 Virtual Threads [Video]

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: