Function Try Blocks
Learn about Function Try Blocks, and their importance in managing exceptions in constructors
Previously, we have seen how we can have try
and catch
blocks inside a function:
#include <iostream>
#include <stdexcept>
void MyFunction() {
try {
throw std::runtime_error { "Oops!" };
} catch (std::runtime_error& e) {
std::cout << "Error: " << e.what();
}
}
int main() {
MyFunction();
}
Error: Oops!
A function try block is a special type of try-catch block that is used to catch exceptions thrown anywhere within the function that it is applied to. In this article, we will explain the basics of function try blocks, and when they're useful.
Using Function Try Blocks
There is an alternative syntax for this, where we want the try
and catch
to apply to our whole function. It looks like this:
#include <iostream>
#include <stdexcept>
void MyFunction() try {
throw std::runtime_error { "Oops!" };
} catch (std::runtime_error& e) {
std::cout << e.what();
}
int main() {
MyFunction();
}
Error: Oops!
As with regular try-catch blocks, we can have multiple catch
statements, to catch different types of errors thrown within our function body:
#include <iostream>
#include <stdexcept>
void MyFunction() try {
throw std::runtime_error { "Oops!" };
} catch (std::logic_error& e) {
std::cout << "Logic error!";
} catch (std::runtime_error& e) {
std::cout << "Runtime error!";
}
int main() {
MyFunction();
}
Runtime error!
As usual, the exception we're catching doesn't need to be directly thrown within the associated body. We can catch exceptions that bubbled up the call stack:
#include <iostream>
#include <stdexcept>
void ThrowException() {
throw std::runtime_error{"Oops!"};
}
void MyFunction() try {
ThrowException();
} catch (std::runtime_error& e) {
std::cout << "Runtime error!";
}
int main() {
MyFunction();
}
Runtime error!
Using Function Try Blocks with Return Types
Where the function has a non-void return type, all of the catch
statements will also need to respect that, by returning an appropriate object:
#include <exception>
int Divide(int x, int y) try {
if (y == 0) {
throw std::invalid_argument{
"Cannot divide by zero"};
}
return x/y;
} catch (std::invalid_argument& e) {
return -1;
}
Exceptions in Member Initializer Lists
Naturally, we can get the same behavior of a function try block by simply having the entire body of the function be a try-catch block.
So in the previous examples, the function try block isn't helping us achieve anything we couldn't before - although it could be argued that the syntax is more readable.
However, some exceptions can only be caught by a function try block: specifically, exceptions that occur in member initializer lists.
Member Initializer Lists
This lesson introduces Member Initializer Lists, focusing on their advantages for performance and readability, and how to use them effectively
In the following example, the Enemy
constructor is going to receive an exception. However, the exception is coming from the member initializer list, outside of the function body:
#include <iostream>
#include <stdexcept>
int GetHealth(int Health) {
if (Health < 0) throw std::logic_error{
"Health cannot be negative"};
return Health;
}
class Enemy {
public:
Enemy(int Health)
: mHealth{GetHealth(Health)} {}
private:
int mHealth;
};
int main() {
Enemy Goblin { -100 };
}
terminate called after throwing an instance of std::logic_error
When a member initializer list throws an exception, a function try block is the only way to catch that exception:
#include <iostream>
#include <stdexcept>
int GetHeath(int Health) {/*...*/}
class Enemy {
public:
Enemy(int Health) try
: mHealth{GetHealth(Health)} {
// ... Constructor body
} catch (std::logic_error& e) {
std::cout << e.what();
}
private:
int mHealth;
};
int main() {
Enemy Goblin { -100 };
}
We now get our custom error message:
Health cannot be negative
terminate called after throwing an instance of 'std::logic_error'
Handling Member Initializer List Exceptions
The previous output shows that our program is still terminated, even though we caught the exception.
When an exception is thrown as part of a constructor's function-try block, it is impossible to recover from that within the associated catch
blocks.
We need to re-throw the exception, or throw a different exception. If we don't rethrow the exception, it will be implicitly rethrown for us.
Either way, an exception escapes from the constructor, and needs to be handled elsewhere in the stack:
#include <iostream>
#include <stdexcept>
int GetHeath(int Health) {/*...*/}
class Enemy {/*...*/}
int main() {
try {
Enemy Goblin { -100 };
} catch (std::exception& e) {
std::cout << "\nGoblin construction failed";
}
std::cout << " but we recovered";
}
Health cannot be negative
Goblin construction failed but we recovered
Therefore, the use of function try blocks within constructors has two purposes:
- To change the type of error thrown, by rethrowing a different type
- For secondary effects - eg, reporting the error to some tracker
Summary
In this lesson, we explored the concept and application of function try blocks in C++. In particular, we emphasized their unique ability to handle exceptions from member initializer lists in constructors. The key takeaways include:
- Function try blocks are special try-catch constructs that can catch exceptions thrown anywhere within a function, including member initializer lists.
- In constructors, function try blocks are the only means to catch exceptions arising from member initializer lists.
- When an exception is caught in a constructor's function-try block, it cannot be fully recovered within the catch block; it must be re-thrown or replaced with another exception.
- Function try blocks in non-void functions require that all catch blocks respect the function's return type.
- The primary uses of function try blocks in constructors are to modify the type of thrown exception and to perform secondary actions, like error logging.
Static Arrays using std::array
An introduction to static arrays using std::array
- an object that can store a collection of other objects