Conversations: Baseless Exceptions

Implicit conversion sequences can be quite useful. But there are... well... exceptions to when they are applied.


September 01, 2002
URL:http://www.drdobbs.com/conversations-baseless-exceptions/184403838

September 2002 C++ Experts Forum/Conversations


"Well, well, well," I said to myself with only a touch of smugness. "Looks like I've found a compiler bug."

I had been chasing down a problem where an exception wasn't being properly caught by the exception handler. I had boiled it down to a small code fragment:

class Base
{
// ... whatever ...
};

class Derived : private Base
{
  void f() {
    try {
      throw Derived();
    }
    catch (Base &) {
      cout << "Caught Base" << endl;
    } catch (...) {
      cout << "Caught something else" << endl;
    }
  }
};

I was expecting the program to implicitly convert the Derived object to a Base &, so it would print out "Caught Base." Instead, though, it was ignoring the conversion and printing out "Caught something else." Gloating over my discovery about the compiler's error, I remembered the words of the Guru: "Remember, my young apprentice, never assume either you or your compiler is correct. Always read the Holy Standard to confirm your understanding and wisdom." A few minutes later, my gloating had turned to puzzlement, because the Standard said my compiler was behaving correctly:

Unambiguous public base class? Well, that seems pretty unreasonable, I thought. After all, the Derived can be converted to a Base, because it's in the scope of Derived. To test it, I added the statement

Base & testRef = *this;

just before the try statement. Sure enough, it compiled without any problems.

"You seem to be meditating upon a puzzlement, my apprentice." I jumped slightly — I didn't think I'd ever get used to the Guru's sudden appearances.

"Uh, yeah," I muttered. "I don't understand why the Standard doesn't allow the exception to be converted, even though it's allowable within the scope."

She carefully marked her place and closed the tome she was carrying. "For the conversion on your 'Base & testRef = *this;' statement, when does the compiler determine whether the conversion is allowed, due to access privileges or inheritance?"

"Well, it does it when it's constructing the reference. No, wait — that's wrong. It does it at compile time."

"Correct, my child. Now, when does the compiler check these requirements for the conversion of your exception handler?"

"It would happen at compile time too, wouldn't it?" The Guru did not respond. Instead, she cocked an eyebrow, and her grey eyes bored into me — her "Are you sure about that?" expression. I thought desperately for a moment. Then the penny dropped. "Oh, of course," I said. "The evaluation has to happen at run time. Any kind of an exception object can be thrown, so the exception handler has to look at the actual object to determine its type."

"And where is access information stored at run time?" the Guru prompted.

"It's stored...." I paused a moment. "Hey, that's a trick question — access checking is done at compile time, no run-time information should be stored. Ah, I get it now — the restriction is there because of the lack of access information."

I expected the Guru to walk away at this point. She didn't, though. She simply stood serenely beside my cubicle. I pondered the issue some more.

"Wait a second," I said slowly. "No, I don't get it. The program still has to know whether or not there's an unambiguous, public base class accessible for conversion. What about this..." I turned to my whiteboard and wrote out a more complicated example:

#include<iostream>
using namespace std;
class Base
{
public:
  virtual ~Base(){}
};

class DerivedPrivately : private Base
{
  // ... whatever ...
};

class DerivedPublicly : public Base
{
  // ... whatever ...
};

void g(int which)
{
  if (which < 0) throw DerivedPublicly();
  else if (which == 0) throw Base();
  else throw DerivedPrivately();
}

void f(int value)
{
  try {
    g(value);
  }
  catch(Base &)
  {
    cout << "Caught Base" << endl;
  }
  catch ( ... )
  {
    cout << "Caught something else" << endl;
  }
}

int main()
{
  f(-1);
  f(0);
  f(1);
}

"If I understand the Standard properly, the output should be 'Caught Base' for the first two calls, and 'Caught something else' on the last call. The compiler is obviously doing some kind of run-time checking, so why limit it to public, unambiguous base classes?"

"Ah, my child, consider if you change your privately derived class, by adding a friend void f(int); statement. What would you expect then?"

"Well, the DerivedPrivately object has declared f() a friend, so I'd expect it to print out 'Caught Base'."

She smiled. "Now, what information will the program require in order to know that the conversion is acceptable?"

Have I ever mentioned how annoying the Guru's Socratic method can be sometimes? I pondered for a moment. "Well," I ventured, "Obviously it has to keep track of the inheritance tree. And I guess it has to keep track of friends, too. That could be done fairly easily, though — just keep some sort of table in f()'s object file."

"Ah, but what if f() doesn't know that it is a friend of DerivedPrivately? That is to say, f()'s translation unit refers only to Base, and it does not include the header file wherein DerivedPrivately is declared. No, my child, the information would have to be stored globally, and the program would require some mechanism that would allow it to map specific machine code to a particular function, in order to determine if the function it is executing is a friend. In a complex heirarchy, which might have any number of friends or implicit conversion sequences, keeping track of such information would represent a significant overhead, which would otherwise be unneccessary. It is far simpler to keep a list of unambiguous, public base classes.

"You will note, by the way, that the same restriction applies to catching pointers. The compiler will only convert a pointer if the type you want to convert it to is an unambiguous public base class of the original type. Pointers do have the added ability to perform qualification conversions."

The Guru turned and glided away. "The solution to your problems should be obvious by now, my apprentice. Exception handling, by the way, is one of only two places where the Holy Standard requires run-time checking of access privileges. The other place, of course, is ..." but she turned a corner, and I lost the rest of what she was saying.

Naturally, the Guru was right. The solution to my problem was simple: add an exception handler for Derived:

class Derived : private Base
{
  void f() {
    try {
      throw Derived();
    }
    catch (Derived &) {
      cout << "Caught Derived" << endl;
    } catch (Base &) {
      cout << "Caught Base" << endl;
    } catch (...) {
      cout << "Caught something else" << endl;
    }
  }
};

Note

[1] ISO C++ Standard, clause 15.3 paragraph 3.

Jim Hyslop is a senior software designer at Leitch Technology International Inc. He can be reached at [email protected].

Herb Sutter (<www.gotw.ca>) is secretary of the ISO/ANSI C++ standards committee, author of the acclaimed books Exceptional C++ and More Exceptional C++, and one of the instructors of The C++ Seminar (<www.gotw.ca/cpp_seminar>). In addition to his independent writing and consulting, he is also C++ community liaison for Microsoft.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.