Exceptions in destructors.

In C++, throwing exceptions in destructors are generally considered a bad idea. This is because the destructor might have already been triggered due to an exception and throwing another one will instantly kill the program. I think I have found one of the rare cases it’s OK.

I’ve started using the rather nice sqlite_modern_cpp library for interfacing with sqlite3. The library uses modern C++ and allows a very nice inline syntax, for example:


database db("tmp.db");

db << "CREATE TABLE a (a INTEGER, b STRING);";
db << "INSERT INTO a VALUES (?,?);" << 1 << 2;   //This line is interesting
db << "SELECT * FROM a;" >> [](int a, string b)
{
    cout << a << ",  " << b << endl;
};

I find the library very pleasant to use. It’s very low friction and things such as named parameters are actually easier than the “simple” way which risks a visit from Bobby Tables. The extractor syntax is also very nice with the lambda above being called once per row.

The system works (see the interesting line) by operator<<(database, auto) creating a database_binder object and preparing the SQL statement. Further calls to operator<<(database_binder&&, auto) bind values to the parameters. When the line is finished, the database_binder destructs and that causes the SQL command to be executed. Naturally we want errors flagged, and SQL errors throwing exceptions would be ideal.

(Note extracting things also causes execution, but since that happens before destruction it’s easier to deal with).

I think this is one example where it’s legitimate to have destructors throw exceptions. Here’s how it works:

operator>> causes a flag to be set indicating that execution has begun. The destructor checks for this flag being unset before executing the query. This stops double execution of a query that aborts part way through due to an exception being thrown in the extraction function. That makes this code safe:

db << "some sql;" >> [](int){ throw 1;};

Secondly the destructor also checks for std::current_exception and won’t execute the query if it exists. The main reason for this is not so much to prevent double exceptions as much as to prevent execution of failed statements. Consider:

db << "some sql (?); " << (throw 1, 2.0);

Depending on the order of evaluation of function arguments, the database_binder can be created before the exception is thrown. If an exception is thrown while we’re building the binder, it makes no sense to try to execute it. I believe GCC evaluates right to left, so it will never actually construct the object, but some versions of VS can do left to right (depending on the optimization level).

The result of the code is that it will always throw exceptions when you’d expect them and will never cause an abort due to double exceptions. The code is here:

https://github.com/edrosten/sqlite_modern_cpp/commit/8b6cfb2af6eb526100ef62579ca6658d359f789a

Edit: I’m now a committer on the official repo https://github.com/aminroosta/sqlite_modern_cpp

Advertisements