Keywords: C++ | Program Termination | RAII | Stack Unwinding | Exception Handling | Object Cleanup
Abstract: This article provides a comprehensive examination of various methods for terminating C++ programs, focusing on the RAII mechanism and stack unwinding principles. It compares differences between termination approaches like return, throw, and exit, demonstrates the importance of object cleanup through detailed code examples, explains why std::exit should be used cautiously in C++, and offers recommended termination patterns based on exception handling to help developers write resource-safe C++ code.
RAII Mechanism and the Importance of Object Cleanup
C++ employs a programming paradigm called RAII (Resource Acquisition Is Initialization), which ensures that objects perform initialization in their constructors and cleanup in their destructors. For example, the std::ofstream class opens a file during construction, allows users to perform output operations, and then, at the end of its lifecycle (typically determined by scope), the destructor is called to close the file and flush any written content to disk.
Failure to invoke the destructor for flushing and closing the file may result in incomplete data writes. Consider the following code example:
#include <fstream>
#include <exception>
#include <memory>
void inner_mad()
{
throw std::exception();
}
void mad()
{
auto ptr = std::make_unique<int>();
inner_mad();
}
int main()
{
std::ofstream os("file.txt");
os << "Content!!!";
int possibility = /* either 1, 2, 3 or 4 */;
if(possibility == 1)
return 0;
else if(possibility == 2)
throw std::exception();
else if(possibility == 3)
mad();
else if(possibility == 4)
exit(0);
}
Behavior analysis for each possibility:
- Possibility 1: Returning from the current function scope allows the system to recognize the end of
os's lifecycle, invoking its destructor for proper cleanup, including closing and flushing the file. - Possibility 2: Throwing an exception also handles the lifecycle of objects in the current scope, performing appropriate cleanup.
- Possibility 3: Stack unwinding mechanism activates; even though the exception is thrown in
inner_mad, the unwinder traverses the stack frames ofmadandmainto execute proper cleanup, with all objects (includingptrandos) being correctly destructed. - Possibility 4:
exit, as a C function, is incompatible with C++ paradigms and does not perform cleanup for objects in the current scope (includingos), potentially leaving the file improperly closed and content unwritten. - Other Possibilities: Leaving the main scope executes an implicit
return 0, having the same effect as Possibility 1, with proper cleanup.
Recommended Program Termination Methods
Return from main
Use this method whenever possible; always prefer to end the program by returning an appropriate exit status from main. The program's caller and possibly the operating system may need to know whether the program successfully completed its intended tasks. For this reason, return zero or EXIT_SUCCESS to indicate successful termination and EXIT_FAILURE to indicate abnormal termination; any other return values are implementation-defined.
However, returning from deep within the call stack can be cumbersome.
Throw an exception and catch it in main
Throwing an exception performs proper object cleanup through stack unwinding, calling destructors for objects in each preceding scope. However, note that when a thrown exception is not caught or when the call stack includes a noexcept function, whether stack unwinding occurs is implementation-defined. According to the C++ standard, in such cases, std::terminate() is called, and stack unwinding may not happen.
Therefore, catch the exception in main and return an exit status:
int main()
{
try
{
// Code that may return by throwing an exception
}
catch(const std::exception&)
{
return EXIT_FAILURE;
}
}
It is advisable to use a custom exception type for intentional throws, such as return_exception.
Termination Methods to Avoid
std::exit
std::exit does not perform any stack unwinding; alive objects on the stack do not invoke their respective destructors for cleanup. The C++ standard explicitly states: terminating the program without leaving the current block (e.g., by calling std::exit) does not destroy any objects with automatic storage duration. If std::exit is called to end a program during the destruction of an object with static or thread storage duration, the program has undefined behavior.
Other Alternatives
There are other ways to terminate a program (besides crashing), but they are not recommended:
std::_Exit: Causes normal program termination, and that is all.std::quick_exit: Causes normal program termination and callsstd::at_quick_exithandlers; no other cleanup is performed.std::exit: Causes normal program termination and then callsstd::atexithandlers; other cleanups are performed, such as calling static object destructors.std::abort: Causes abnormal program termination; no cleanup is performed. This should be called if the program terminates in a very unexpected way; it only signals the OS about the abnormal termination, and some systems may perform a core dump.std::terminate: Calls thestd::terminate_handler, which by default callsstd::abort.
Destruction of Static and Thread Objects
When exit is called directly or after a return from main, thread objects associated with the current thread are destroyed. Next, static objects are destroyed in the reverse order of their initialization (after calls to functions specified to atexit, if any). The following example illustrates how such initialization and cleanup work:
#include <stdio.h>
class ShowData {
public:
ShowData(const char *szDev) {
errno_t err;
err = fopen_s(&OutputDev, szDev, "w");
}
~ShowData() { fclose(OutputDev); }
void Disp(char *szData) {
fputs(szData, OutputDev);
}
private:
FILE *OutputDev;
};
ShowData sd1 = "CON";
ShowData sd2 = "hello.dat";
int main() {
sd1.Disp("hello to default device\n");
sd2.Disp("hello to file hello.dat\n");
}
In this example, static objects sd1 and sd2 are created and initialized before entering main. After the program terminates using the return statement, sd2 is destroyed first, followed by sd1. The destructor of the ShowData class closes the files associated with these static objects.
An alternative approach is to declare ShowData objects with block scope, which implicitly destroys them when they go out of scope:
int main() {
ShowData sd1("CON"), sd2("hello.dat");
sd1.Disp("hello to default device\n");
sd2.Disp("hello to file hello.dat\n");
}
Conclusion
Properly terminating a C++ program is crucial, especially when resource management is involved. Prefer returning from main or throwing an exception and catching it in main to ensure the RAII mechanism functions correctly and objects are properly cleaned up. Avoid using std::exit and other termination functions that do not perform complete cleanup to prevent resource leaks and data inconsistency issues.