简体   繁体   中英

Is D's scope failure/success/exit necessary?

When using a language that has try/catch/finally, are D's failure/success/exit scope statements still useful? D doesn't seem to have finally which may explain why those statements are used 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.

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.

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.

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();
}

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]).

It worth to mention that scope(exit), scope(failure) and scope(success) are also available for C++.

Following syntax is supported, case 1:

try
{
    int some_var=1;
    cout << "Case #1: stack unwinding" << endl;
    scope(exit)
    {
        cout << "exit " << some_var << endl;
        ++some_var;
    };
    scope(failure)
    {
        cout << "failure " << some_var  << endl;
        ++some_var;
    };
    scope(success)
    {
        cout << "success " << some_var  << endl;
        ++some_var;
    };
    throw 1;
} catch(int){}

prints:

Case #1: stack unwinding
failure 1
exit 2

Case 2:

{
    int some_var=1;
    cout << "Case #2: normal exit" << endl;
    scope(exit)
    {
        cout << "exit " << some_var << endl;
        ++some_var;
    };
    scope(failure)
    {
        cout << "failure " << some_var << endl;
        ++some_var;
    };
    scope(success)
    {
        cout << "success " << some_var << endl;
        ++some_var;
    };
}

prints:

Case #2: normal exit
success 1
exit 2

@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)).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM