When using a language that has try/catch/finally is D's failure/success/exit scope statements still useful? D doesnt seem to have finally which may explain why those statements are using in D. But with a language like C# is it useful? I am designing a language so if i see many pros i'll add it in.
try/catch/finally forces a level of nesting; scope guards don't. Besides, they let you write cleanup code in the same "area" as allocation code, so no more "open file, scroll to end of function, close file, scroll to top of function".
Fundamentally though, it's just a more convenient expression of try/catch/finally exception handling - anything you can do with try/catch/finally you can do with scope guards, and reverse.
Is it worth it? I'm a D fanboy (so, biased), but I'd say definitely.
Distinguishing failure-exit from success-exit is quite useful some of the time -- I have no real world experience with D, but Python's with
statement also allows that, and I find it very useful, for example, to either commit or rollback a DB transaction that was opened in the protected part of the body.
When I explained this then-new Python feature (it's been around for a while now;-) to friends and colleagues who are gurus in C++ and Java I found they immediately understood, and saw the interest in having such a feature (Python does have finally
, too, but that's no help in distinguishing success from failure, just like in other languages [or C++'s "RAII destruction of auto variables in the block" equivalent]).
Disclaimer I'm a D fan boy too.
someRiskyFunctionThatMayThrow();
lock();
/* we have definitly got the lock so lets active
a piece of code for exit */
scope(exit)
freelock();
Compared to:
try
{
someRiskyFunctionThatMayThrow();
lock();
}
finally
{
freeLockIfNotGot();
}
scope(X)
isn't necessary in the same way that for
isn't necessary provided you have if
and goto
.
Here's a paraphrased example from some code I've been writing today:
sqlite3* db;
sqlite3_open("some.db", &db);
scope(exit) sqlite3_close(db);
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, "SELECT * FROM foo;", &stmt);
scope(exit) sqlite3_finalize(stmt);
// Lots of stuff...
scope(failure) rollback_to(current_state);
make_changes_with(stmt);
// More stuff...
return;
Contrast this to using try/catch:
sqlite3* db;
sqlite3_open("some.db", &db);
try
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, "SELECT * FROM foo;", &stmt);
try
{
// Lots of stuff...
try
{
make_changes_with(stmt);
// More stuff...
}
catch( Exception e )
{
rollback_to(current_state);
throw;
}
}
finally
{
sqlite3_finalize(stmt);
}
}
finally
{
sqlite3_close(db);
}
The code has turned into spaghetti, spreading the error recovery all over the shop and forcing a level of indentation for every try block. The version using scope(X) is, in my opinion, significantly more readable and easier to understand.
@DK, It should be pointed out, in C++ (and Java I think) you could easily use an "anonymous" class to accomplish the same thing as scope(exit):
int some_func()
{
class _dbguard { sqlite3* db;
_dbguard(const _dbguard&); _dbguard& operator=(const _dbguard&);
public:
_dbguard(const char* dbname) { sqlite3_open(dbname, &db);}
~_dbguard() {sqlite3_close(db);}
operator sqlite3*() { return db; }
} db("dbname");
...
}
And if you did this more than once you'd immediately turn it into a full class to handle your RAII for you. It is so simple to write I can't imagine a C++ program that uses sqlite (as used in the example) without creating classes like CSqlite_DB and CSqlite_Stmt. In fact the operator sqlite3*() should be anathama and the full version would just have methods that provide statements:
class CSqlite3_DB {
...
CSqlite3_Stmt Prepare(const std::string& sql) {
sqlite3_stmt* stmt = 0;
try {
sqlite3_prepare_v2(db, sql.c_str(), &stmt);
} catch (...) {}
return stmt;
}
};
As for the original question, I'd say the answer is "not really". Proper respect for DRY would tell you to take those long blocks of try/catch/finally and convert them to separate classes that hide the try/catch parts away from the rest where they can (in the case of scope(failure)) and makes resource management transparent (in the case of scope(exit)).