std::terminate and the noexcept specifier

This lesson explores the std::terminate function and noexcept specifier, with particular focus on their interactions with move semantics.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we will delve into two important aspects of error handling: the std::terminate() function and the noexcept specifier. We will explore:

  • What std::terminate() is and when it is used.
  • How to customize the behavior of std::terminate() using std::set_terminate().
  • The significance of the noexcept specifier and its impact on functions.
  • Practical implications of noexcept in move semantics, and the std::move_if_noexcept() function.

std::terminate()

As we’ve seen in some previous examples, when our application has an unhandled exception, it terminates. "Terminate" has a specific meaning in this context - it refers to invoking the std::terminate() function.

We don’t need to be using exceptions to use this function - we can just call it directly after including the <exception> header:

#include <exception>

void MyFunction() {
  std::terminate();
}

Calling std::terminate will bypass any possible catch statements that might attempt recovery.

#include <exception>
#include <iostream>

void MyFunction() {
  std::terminate();
}

int main() {
  try {
    MyFunction();
  } catch (...) {
    std::cout << "Caught!";
  }
}

Note that our catch block was not executed:

example.exe (process 69992) exited with code 3.

std::terminate() is designed for when our code encounters an error it knows is irrecoverable, meaning the process should just end. From our output, we can note that this is reflected by our exit code. Any non-zero exit code indicates an error:

std::set_terminate()

By default, std::terminate() simply calls another function, std::abort(), to close our application.

However, we can define our program’s terminate handler ourselves, by passing a function to std::set_terminate().

Below, we define a function called CustomTerminate(). We then pass it to std::set_terminate(), registering this function as our terminate handler:

#include <exception>
#include <iostream>

void CustomTerminate() {
  std::abort();
}

int main() {
  std::set_terminate(CustomTerminate);
  throw 1;
}
example.exe (process 69992) exited with code 3.

Passing a Function to a Function

The previous example is the first time in the course that we’ve provided a function (CustomTerminate) as an argument to another function (std::set_terminate).

We can do this, as C++ implements a programming paradigm known as first-class functions. This concept lets us use functions in much the same way we can any other type. For example, functions can be stored in variables, passed to other functions, and returned from functions.

We have a dedicated chapter that covers these mechanisms in more detail later in the course.

Within our terminate handler, we can’t try to recover and keep our program running. Our custom handler still needs to call std::abort().

However, it doesn’t have to call std::abort() immediately. We can implement any behaviour we need before closing our program:

#include <exception>
#include <iostream>

void CustomTerminate() {
  std::cout << "Terminating!";
  std::abort();
}

int main() {
  std::set_terminate(CustomTerminate);
  throw 1;
}
Goodbye!
example.exe (process 69992) exited with code 3.

In large-scale projects, terminate handlers are most commonly used to generate reports containing additional information about the crash, which may help us debug.

They can include information like what the unhandled exception was, the value of important variables, the call stack, hardware configuration, and more.

The noexcept Specifier

Similar to how we can mark functions with annotations like const and override, so too can we mark them noexcept:

void SomeFunction() noexcept {
  // ...
}

This specifier tells the compiler (and other developers) that this function will not throw any exceptions to their caller. This means either:

  • no exceptions will be thrown, or
  • any exceptions that are thrown will be handled internally, without leaking up to the caller

This is enforced at run time, rather than compile time. If a noexcept function leaks an exception, our program will std::terminate(), rather than giving the caller the opportunity to catch the exception.

The noexcept specifier can be applied to any function where we want this behavior. However, its most important use case, and the reason it was originally added to the language, is to solve a specific problem at the intersection of exceptions and move semantics. We cover this problem in the next section.

Conditional noexcept

It is possible to pass a boolean to the noexcept specifier, to determine whether or not it applies.

For example, noexcept(true) is equivalent to noexcept, and noexcept(false) is equivalent to not having the specifier there at all.

We can pass any boolean expression to this handler, as long as it can be evaluated at compile time.

constexpr bool SomeBoolean{true};
void SomeFunction() noexcept(SomeBoolean) {
  // ...
}

This is most often used in template code, where whether a function is noexcept or not depends on the types our template was instantiated with.

Exceptions and Move Semantics

In our earlier lesson on move semantics, we introduced the idea of the move constructor and move assignment operator.

These functions let us define how our objects can be moved to another location. They are designed to be fast, but they leave the original object in an indeterminate state, often called the moved-from state.

In most cases, this isn’t a problem, because once the move completes, we’re not going to be using the original object anymore. However, what if the move operator or constructor throws an exception, mid-way through the process?

At the point the exception is thrown, the original object may already have been compromised, but the new object hasn’t fully been created yet. So, both of the objects are in an indeterminate state.

To handle this scenario, alongside move semantics in C++11, the language also introduced the noexcept specifier and the move_if_noexcept() function.

std::move_if_noexcept()

In our move semantics lesson, we saw how we could use std::move to cast an object to an rvalue reference. This is how we indicate an object can be moved-from, allowing the surrounding expression to use move semantics.

Below, we create a MyType with such a reference, prompting the more efficient move constructor to be used:

#include <iostream>

struct MyType {
  MyType() = default;
  MyType(const MyType& Other) {
    std::cout << "Copy Constructor";
  }

  MyType(MyType&& Other) {
    std::cout << "Move Constructor";
  }
};

int main() {
  MyType Original;
  MyType New{std::move(Original)};
}
Move Constructor

However, this move constructor does not specify noexcept. This means that it may throw an exception, which the calling function (main, in this case) will have to deal with.

Given the complexities of dealing with exceptions during move operations we discussed earlier, the calling function probably won’t want to deal with that.

Instead, it can switch to use std::move_if_noexcept(). This function will only cast our object to an rvalue reference if the move constructor is marked noexcept.

In our case, it isn’t, so our program falls back to the safer copy constructor:

#include <iostream>

struct MyType {
  MyType() = default;
  MyType(const MyType& Other) {
    std::cout << "Copy Constructor";
  }

  MyType(MyType&& Other) {
    std::cout << "Move Constructor";
  }
};

int main() {
  MyType Original;
  MyType New{std::move_if_noexcept(Original)};
}
Copy Constructor

If we correctly mark our move constructor as noexcept, our move_if_noexcept() example switches back to using move semantics as we’d expect:

#include <iostream>

struct MyType {
  MyType() = default;
  MyType(const MyType& Other) {
    std::cout << "Copy Constructor";
  }

  MyType(MyType&& Other) noexcept {
    std::cout << "Move Constructor";
  }
};

int main() {
  MyType Original;
  MyType New{std::move_if_noexcept(Original)};
}
Move Constructor

When to apply noexcept

In general, most of the functions we write won’t be throwing exceptions to their caller. So should we mark all of these functions noexcept?

As usual, opinions differ, and different teams will have their guidelines on what approach to take. They broadly fall into one of two camps:

  • Apply noexcept everywhere it is correct, or
  • Apply noexcept only where it is useful

We use the second approach in this course. In general, this means we apply noexcept to almost every implementation of move semantics, and pretty much nowhere else.

noexcept with Data Structures and Algorithms

Implementing move semantics with noexcept is useful even if we’re not using it directly. This is because, behind the scenes, many of the data structures and algorithms we use will be checking for noexcept, and only using our move semantics if it is applied.

For example, when std::vector resizes, it needs to move all of our objects to a new location.

This is implemented using std::move_if_noexcept meaning that, if our move constructor isn’t marked noexcept, we won’t get the benefit of having implemented it.

Below, we create a std::vector with one object, then prompt the vector to move to a new location using reserve().

Because our move constructor isn’t marked noexcept, the vector doesn’t use it when moving our object, preferring the slower copy constructor:

#include <iostream>
#include <vector>

struct MyType {
  MyType() = default;
  MyType(const MyType& Other) {
    std::cout << "Copy Constructor";
  }

  MyType(MyType&& Other) { // missing noexcept
    std::cout << "Move Constructor";
  }
};

int main() {
  std::vector<MyType> V;
  V.emplace_back();
  V.reserve(100);
}
Copy Constructor

Adding noexcept to our move constructor now means the standard library algorithm will use it:

#include <iostream>
#include <vector>

struct MyType {
  MyType() = default;
  MyType(const MyType& Other) {
    std::cout << "Copy Constructor";
  }

  MyType(MyType&& Other) noexcept {
    std::cout << "Move Constructor";
  }
};

int main() {
  std::vector<MyType> V;
  V.emplace_back();
  V.reserve(100);
}
Move Constructor

Summary

In this lesson, we took an in-depth look at std::terminate() and the noexcept specifier. Key takeaways include:

  • std::terminate: A function for handling irrecoverable errors, terminating the program in a controlled manner. We learned how to invoke std::terminate directly and how it interacts with unhandled exceptions.
  • Custom Termination with std::set_terminate: We explored customizing the termination process using std::set_terminate(), allowing for additional actions like logging or cleanup before terminating.
  • The Role of noexcept: A specifier that ensures a function does not throw exceptions to its caller, which is enforced at run time. If a noexcept function leaks an exception, our program terminates.
  • noexcept in Move Semantics: We examined how noexcept is particularly useful in move semantics, particularly with std::move_if_noexcept.

Was this lesson useful?

Next Lesson

Storing and Rethrowing Exceptions

Learn how we can rethrow errors, and wrap exceptions inside other exceptions using standard library helpers.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

std::terminate and the noexcept specifier

This lesson explores the std::terminate function and noexcept specifier, with particular focus on their interactions with move semantics.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Exceptions and Error Handling
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Storing and Rethrowing Exceptions

Learn how we can rethrow errors, and wrap exceptions inside other exceptions using standard library helpers.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved