Thanks for sharing the videos, Konstantin.
> Unpredictable code can be dangerous:
>
> void boil_kettle()
>
{
> i2c_set_bit(10, 1); // bit 10 is stove heater control, turn it on
> foo(); // wait for kettle to
start boiling
> i2c_set_bit(10, 0); // turn heater off
> }
>
> If foo() starts throwing exceptions, I
will likely have a fire in my kitchen!
>
Indeed, that would certainly be problematic. However, there is a
way to get around this while not having to wrap everything in try ... catch ... blocks with more C++
machinery using the catch-all handler and RAII.
To quickly expand upon your example:
=== main.cpp ===
#include <iostream>
#include <unistd.h>
void i2c_set_bit(int bit, int val) {
std::cout << "Bit " << bit <<
" set to " << val << "\n";
}
class i2c_temp_bit {
int bit; /* which bit */
int newval; /* value to set bit
to */
int oldval; /* value to reset bit to */
public:
i2c_temp_bit(int b, int nv, int ov)
: bit{b},
newval{nv}, oldval{ov}
{
std::cout << "Bit " << bit << " set to " << newval << "\n";
}
~i2c_temp_bit()
{
std::cout << "Bit " << bit << " set to " << oldval << "\n";
}
i2c_temp_bit(const i2c_temp_bit&) =
delete;
i2c_temp_bit(i2c_temp_bit&&) = delete;
i2c_temp_bit& operator=(const i2c_temp_bit&) = delete;
i2c_temp_bit& operator=(i2c_temp_bit&&) = delete;
};
int check_kettle_temp() {
static int callnum = 0;
if
(++callnum > 5) {
return 100;
}
#ifdef BADNUM
else if (callnum == 4) { /* Don't like this number */
throw callnum;
}
#endif
else {
return 25;
}
}
void foo() {
std::cout << "Waiting for kettle to boil"
<< std::endl;
do { /* Check every 100 ms */
usleep(100000);
} while (check_kettle_temp() < 100);
std::cout << "Water is boiled!" << std::endl;
}
void boil_kettle()
{
#ifdef RAII
i2c_temp_bit temp(10, 1,
0);
foo();
#else
i2c_set_bit(10, 1); // bit 10 is stove heater control, turn it on
foo(); // wait for
kettle to start boiling
i2c_set_bit(10, 0); // turn heater off
#endif
}
int main()
{
try {
boil_kettle();
} catch (...) {
std::cerr << "Abnormal termination\n";
}
}
=== main.cpp ===
When we run
this:
$ g++ -o main main.cpp && ./main
Bit 10 set to 1
Waiting for kettle to boil
Water is boiled!
Bit 10
set to 0
This is as expected, of course.
But indeed as you say, when foo() can throw an exception, we get a
problem:
$ g++ -DBADNUM -o main main.cpp && ./main
Bit 10 set to 1
Waiting for kettle to boil
Abnormal
termination
The exception is caught by the catch-all handler, but because boil_kettle() never finishes
execution, bit 10 never gets reset to 0 and that's a big problem.
With RAII however, we get this instead:
$
g++ -DBADNUM -DRAII -o main main.cpp && ./main
Bit 10 set to 1
Waiting for kettle to boil
Bit 10 set to 0
Abnormal termination
Because the i2c_temp_bit object is destroyed upon unwinding the boil_kettle() function,
it calls the destructor which resets the bit.
For closure, it does behave identically to the non-RAII
implementation when there are no exceptions:
$ g++ -DRAII -o main main.cpp && ./main
Bit 10 set to 1
Waiting
for kettle to boil
Water is boiled!
Bit 10 set to 0
Personally, I don't like this as it adds way too much
complexity to the code in order to avoid C++ foot-guns (see how the copy and move constructors etc. need to
be deleted, or else requires a bit of thinking to implement properly).
But, it is an alternative that exists
to safely handle these situations. |