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.
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!
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;
}
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.
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'
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:
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:
Learn about Function Try Blocks, and their importance in managing exceptions in constructors
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.