ACCU Home page ACCU Conference Page ACCU 2017 Conference Registration Page
Search Contact us ACCU at Flickr ACCU at GitHib ACCU at Google+ ACCU at Facebook ACCU at Linked-in ACCU at Twitter Skip Navigation

pinA Letter from Ken Hagan annotated by The Harpist

Overload Journal #30 - Feb 1999 + Programming Topics   Author: Ken Hagan

The Harpist:

I know that some readers do not like invasive comments on letters but having thought long an hard about this one I feel that responding to each point as raised makes it easier to follow. I will write in italic Arial and leave Ken's original in plain Times Roman.

Ken Hagan:

We actually agree that unwinding is important. Resources must be released. My point was simply that it doesn't help rollback. I'd be interested if anyone could present code examples that simplify rollback.

TH:

Consider:

class Rollable
{
  string first_m;
  string second_m;
public:
  Rollable(
    string const & item1, 
    string const & item2
    )throw (std::exception)
  : first_m(item1),
    second_m(item2)
  {} 
// other members
};

Now you and I both know that classes like string are difficult to write so that their copy ctors do not throw, (more below) so how should I write a copy assignment for this class. If I leave it to the compiler-generated version it is possible that first_m has been copied (and so the original lost) and the effort to copy second_m results in an exception. How are we going to rollback?

If you are unhappy with my use of string (because of the putative promise that you refer to below) replace it with any UDT whose construction can fail.

The first thing I would do is to make a small design change:

class Rollable
{
  auto_ptr<string> first_m;
  auto_ptr<string> second_m;
public:
  Rollable(
    string const & item1, 
    string const & item2
    ) throw (std::exception)
  :
first_m(auto_ptr<string> (new string(item1))),
second_m(auto_ptr<string> (new string(item2)))
  {} 
// other members
};
Now I can provide:
Rollable & Rollable::operator=
  (Rollable const & rhs)
{
  auto_ptr<string>
    temp1(new string(*(rhs.first_m)));
  auto_ptr<string>
    temp2(new string(*(rhs.second_m)));
// actual copying finished now reseat copies
  first_m = temp1;
  second_m = temp2;
  return *this;
}

I believe that either that copy assignment completes satisfactorily or the left-hand object remains as it was. Maybe we mean different things by commit or rollback (and certainly there are places where the above mechanism might not help. Databases that support change in situ spring to mind.) but I think that the above example follows a simple principle that is easy to teach and follow: do not modify original data until work that may raise exceptions has been completed.

KH:

We also agree that certain functions must be allowed to throw since their return values are otherwise constrained. If I may digress here, Stroustrup says (3rd Ed, 14.4.6.1) that "the standard library assumes proper (non-exception-throwing) behaviour of copy constructors". I think this is forces a reference counted implementation of std::string. I can't see how you'd code a non-throwing string copy constructor otherwise. Indeed, the desirability of non-throwing probably means that most classes which dynamically allocate their state would be better off as reference counted implementations. What do you think?

TH:

I think that you may have misunderstood Stroustrup. I think he meant that such things as vector etc. assume that copying will not throw. In writing that I have just realised why vector<string> may be a problem; exactly because the copy constructor for string might throw bad_alloc.

There is a further problem in that writing reference counted UDTs is hard if they are to be used in a multi-threaded environment. I am not saying that it cannot be done, but most programmers will get it (dangerously) wrong.

KH:

We also agree on static checking. Without it, and especially with the current confusion over whether the ES is part of the type, I think we have a hole in the type system. On the related questions of what ES to apply to template instantiations, I notice that the compiler could deduce the ES of a function in most cases. In the absence of the export keyword, template definitions are fully visible at point of instantiation, and the same logic that checks the ES could presumably generate one.

TH:

But we are far from any such compiler and until more programmers want to use exception specifications we are far from getting one.

KH:

I think our disagreements are centred on two issues. First is our faith in error codes. In the absence of static ES checking, I don't see that it is easier or harder to ignore exceptions or return codes. As you say, it is a matter of education. I believe that ANSI added the (void) cast to C to permit a certain amount of static checking, so perhaps that counts as a "known or imaginable analysis tool" for error codes. Also, even in the presence of an ES, there is still the risk that people will swallow exceptions rather than dealing with them. Now that's something that no analysis tool could ever spot.

TH:

We may have to agree to differ but let me take one last cut. Consider the case of the changed specification for new. If you use new(nothrow) and wilfully or otherwise do not check the return value your program enters undefined behaviour if the object was not constructed through lack of memory (I am not sure that that implementation of new doesn't also trap and discard all exceptions thrown by the constructor. Perhaps one of the standards experts reading this could comment)

However if you use a version of new that throws bad_alloc if there is insufficient memory the program may abort (if the programmer ignored handling the exception) but it should do so without damaging other processes and data.

I do not advocate either behaviour but the latter seems safer to me than the former.

On the subject of swallowing exceptions, that has to be done explicitly and is clearly a different order of reprehensible behaviour than simply being careless.

KH:

Our main disagreement however seems to be the environment we work in. I write COM based software running under Windows. COM interface methods cannot throw exceptions, since the caller may be in a different process or a language that doesn't support the concept. Furthermore, Windows does not tolerate exceptions passing through OS callbacks, like the window procedures that drive every visible entity in the GUI. In such an exception-hostile environment, Francis' EndOfProgram exception would either disappear without trace or bring the system down hideously. I have no choice but to place throw() on all major interfaces.

TH:

As a matter of interest what does a call to exit() do in such code? Or raising a signal? Of course there are program contexts where exceptions must not leak, but how does an error code help?

As you have just told me that COM interfaces must not throw I would have thought that any mechanism that gave a fighting chance that a sane tool provider could produce a tool to detect breaches would be highly welcome. Obviously in the context in which you are working you need a catch(…) firewall to deal with potential exception leakage. But with that firewall in place your COM object would seem to me to be that much safer.

KH:

As an aside, would it be too provocative for me to claim that the standard library is not a major interface? Even if several components are written in C++, and sharing the code to whatever extent the OS allows, they each have to assume the others are written in another language, so they can't pass library structures around to each other. It is an implementation detail, hidden from my clients.

You are a C++ programmer at heart, and you confuse my broader perspective for C-centricity. 8-)

TH:

That is the one place where we must disagree. I am a programmer and always try to find the right tool for the job. C has its place but two of its weaker elements are signal handling and setjmp/longjmp.

Thanks for taking the time to write. I, and I hope many readers, appreciate it.

Overload Journal #30 - Feb 1999 + Programming Topics