Error handling errors are the most common source of error.
The ravings that came to mind when writing this article
The main battles over the fact that it is better to use when programming in C # - exceptions or return codes for processing are gone into the distant past (*), but there are still no other kinds of battles: yes, ok, we stopped at exception handling, but how do we handle them "correctly"?
')
There are many points of view on what is “right”, most of which boils down to the fact that you need to catch only those exceptions that you can handle, and throw all the rest to the calling code. Well, and if an incomprehensible exception came to the upper level in a bold way, then shoot the entire application as a whole, since it is no longer clear whether it is, birth, in a consistent state or not.
There are many pros and cons of this method of intercepting and handling exceptions, but today I want to consider a slightly different topic. Namely, the topic of ensuring a consistent state of the application in the light of the occurrence of an exception - three levels of security exceptions.
Three types of warranty
In the late 1990s, Dave Abrahams offered three levels of security exceptions: a basic guarantee, a strict guarantee, and a guarantee of the absence of exceptions. This idea was warmly received by the C ++ community of developers, and after its popularization (and some modification) with the Sutter coat of arms, the exclusion security guarantees became widely used in boost, in the standard C ++ library, and in application development.
Initially, these guarantees were proposed by Dave Abrahams to implement the STLPort library in C ++, but the very idea of ​​exceptions security is not specific to a programming language and can be used in other languages ​​that use exceptions as the main error handling mechanism, such as Java or C #. In addition, there are currently two versions of the definitions of security guarantees for exceptions: (1) the original version proposed by Dave Abrahams and (2) a modified version popularized by Sutter and Stroustrup, and more suitable not only for libraries, but also for applications.
Basic warranty
The original definition is : “
in the case of exceptions there should be no resource leaks ”.
The current definition is : “
when any exception occurs in a certain method, the state of the program must remain consistent ”. This means not only the absence of resource leaks, but also the preservation of class invariants, which is a more general criterion, compared to the basic definition.
The difference between these two formulations is due to the fact that initially this guarantee was proposed for the implementation of the library in C ++ and had no relation to application applications. But if we talk about a more general case (that is, about an application, and not just about a library), then we can say that resource leaks are only one source of bugs, but far from unique. Preserving the invariant at any stable point in time (**) is a guarantee that no external code can “see” the mismatched state of the application, which, you see, is no less important than the absence of resource leaks. Few users of the banking application will be interested in memory leaks, if, when transferring money from one account to another, money can “go” from one account, but “not reach” another.
Strict warranty
As for the definition of a strict guarantee of exceptions, the original and current definitions are similar and boil down to the following: “
if an exception occurs during the operation, this should not have any impact on the state of the application ”.
In other words, a strict exception guarantee ensures transactional operations when we receive either all or nothing. In this case, when an exception occurs, we must roll back to the state of the application, which was before the operation, and move to a new state only if the entire operation was successfully completed.
Guarantee no exceptions
The assurance of the absence of exceptions comes down to the following: “
under no circumstances will the function generate exceptions ”.
This guarantee is the simplest in terms of definition, however, it is not as simple as it seems. Firstly, it is practically impossible to provide it in the general case, especially in the .Net environment, when an exception can occur at almost any point of the application. In practice, only units of operations follow this guarantee, and it is on the basis of such operations that guarantees of previous levels are built. In C #, then one of the few operations that provide this guarantee is link assignment, and in C ++, the swap function implements the exchange of values. It is on the basis of these functions that a strict exception guarantee is often implemented, when all the “dirty work” is performed in a temporary object, which is then assigned to the resulting value.
Secondly, in some cases it is impossible to ensure the normal operation of other functions, unless some operations follow the guarantee of the absence of exceptions. So, for example, in C ++, in order to provide even a basic guarantee of exceptions (or rather resource leaks) in containers, it is necessary that the destructor of the user type does not generate exceptions.
The three exclusion security guarantees discussed above go from the weakest to the strongest; however, each subsequent warranty is a superset of the previous one. This means that the fulfillment of a strict warranty by an automatic machine entails the fulfillment of a basic guarantee, and the guarantee of the absence of exceptions entails the fulfillment of a strict guarantee. If the code does not even respond to the basic guarantee of exceptions, then it is a time bomb in your application and sooner or later will lead to unpleasant consequences, breaking it up into hell.
Now let's look at a few examples.
Examples of breach of basic warranty
The main way to prevent memory leaks and resources in C ++ is
RAII (Resource Acquisition Is Initialization) idiom, which is that an object captures a resource in the constructor and frees it in the destructor. And since the call to the destructor is performed automatically when the object goes out of scope for any reason, including when an exception occurs, it is not surprising that the same idiom is also used to ensure the safety of exceptions.
In C #, this idiom has migrated as an
IDisposable interface and
using constructs, however, unlike C ++, it is applicable to manage the lifetime of a resource in a certain area of ​​view, and is not suitable for managing the set of resources captured in the designer.
Let's look at this example:
// ,
class DisposableA : IDisposable
{
public void Dispose() {}
}
//
class DisposableB : IDisposable
{
public DisposableB()
{
disposableA = new DisposableA();
throw new Exception( "OOPS!" );
}
public void Dispose() {}
private DisposableA disposableA;
}
// -
using ( var disposable = new DisposableB())
{
// ! Dispose
// DisposableB, DisposableA
}
* This source code was highlighted with Source Code Highlighter .
So, we have two disposable-classes:
DisposableA and
DisposableB , each of which captures some managed resource in the constructor and releases it in the
Dispose method. Let's not consider the finalizer for now, because it will not help us in any way to guarantee a deterministic order of resource release, which in some cases is vital.
In this case, when an exception is thrown by the constructor of the class
DisposableB, we will never call the
Dispose method, because a
disposable object never existed. In this regard, the behavior of most mainstream programming languages ​​is more or less the same, but there are some differences. The similarity lies in the fact that if the constructor "falls", the calling code will not be able to get a reference to the object that has not yet been constructed and to explicitly release its resources. However, unlike C ++, in which the call to the destructor of fully constructed fields is performed automatically, this is not the case in the “controlled” C # language (***). we will get a “resource drain” (or at least their non-deterministic release).
The same problem can manifest itself in a more subtle way. In the case considered earlier, it is clearly seen that we created an instance of the disposable object, after which an exception is generated. But there are cases when the lack of a basic guarantee of exceptions is a little more difficult to see.
class Base : IDisposable
{
public Base()
{
//
}
public void Dispose() {}
}
class Derived : Base, IDisposable
{
public Derived( object data)
{
if (data == null )
throw new ArgumentNullException( "data" );
// OOPS!!
}
}
// -
using ( var derived = new Derived( null ))
{}
* This source code was highlighted with Source Code Highlighter .
Generating an exception in the constructor of the class
Derived violates the basic guarantee of exceptions and leads to a leak of resources, since the
Base class
Dispose method is not called (****). Again, since the compiler knows about the
IDisposable interface only through the prism of the
using construct, in all cases when the disposable object is a field of another class, only the programmer is responsible for calling
Dispose .
In addition to the base class, field initializers can play a similar joke, when the constructor of one of the fields can generate an exception:
class ComposedDisposable : IDisposable
{
public void Dispose() {}
private readonly DisposableA disposableA = new DisposableA();
// , DisposableB ? OOPS!!
private readonly DisposableB disposableB = new DisposableB();
}
* This source code was highlighted with Source Code Highlighter .
In this case, if the constructor of the
DisposableB class when initializing the
disposableB field generates an exception, it will not be possible to intercept it and release the already captured resources. In C ++, there is such a thing as interception of exceptions that occurred in the initialization list (see
Exception and Member Initialization ), however, there is no such possibility in C #, so there is one way out of this situation: try not to allow it.
As for all previous cases, providing a basic guarantee of exceptions completely falls on the shoulders of the developer, since C # does not provide any "sugar" for these purposes.
All we have to do is either create sub-objects in the right order and create a disposable field at the very end of the constructor, or wrap their creation in a try / catch block and clear all resources in the event of an exception.An example of a strict exemption guarantee. Object initializer and collection initializer
The examples of violation of the basic warranty of the exceptions given above, although they are not contrived, are not so often found. And if the C # compiler cannot help us in the case of creating objects containing several managed resources, it can help in some other cases, for example, in creating objects and collections.
The initializer of objects and collections (object initializer and collection initializer) ensure that the object is created and initialized or the collection is filled with a list of items. Let's consider the following example.
class Person
{
public string FirstName { get ; set ; }
public string LastName { get ; set ; }
public int Age { get ; set ; }
}
var person = new Person
{
FirstName = "Bill" ,
LastName = "Gates" ,
Age = 55,
};
* This source code was highlighted with Source Code Highlighter .
At first glance it may seem that this is just syntactic sugar for the following:
var person = new Person();
person.FirstName = "Bill" ;
person.LastName = "Gates" ;
person.Age = 55;
* This source code was highlighted with Source Code Highlighter .
However, in reality,
when the object initializer is called, a temporary variable is created, then the properties of this particular variable are changed, and only then it is assigned to the new object :
var tmpPerson = new Person();
tmpPerson.FirstName = "Bill" ;
tmpPerson.LastName = "Gates" ;
tmpPerson.Age = 55;
var person = tmpPerson;
* This source code was highlighted with Source Code Highlighter .
This ensures the atomicity of the object creation process and the inability to use a partially initialized object in the case of an exception being thrown by one of the setters. A similar principle lies in the initializer of collections, when objects are added to the temporary collection and only after it is filled in, the temporary variable is assigned to the new object.
The principle underlying these two concepts can easily be used in our own implementation of the strict guarantee of exceptions in our own code. To do this, it suffices to perform all changes in the internal state of an object in a certain time variable and only after they are completed will the atomically change its real state.
Conclusion
Correct exception handling is not a simple matter, and, as some examples have shown, sometimes even the basic exception guarantee is difficult. However, the provision of such guarantees is a vital prerequisite when developing applications, since it is much easier to hide the entire complexity of working with resources in one place than to smudge it with a thin layer throughout the entire application. The golden rule, formulated a decade ago by Scott Meyers, is still in force:
create classes that are easy to use correctly and difficult to use incorrectly , and the guarantee of exceptions in this plays clearly not the last role.
If we talk about the practical application of these guarantees, then a few points should be remembered. First, the code that does not perform the basic exception guarantee is incorrect; based on it, it is simply impossible to create an application whose state will not break when it is used or changed (*****). Secondly, do not be paranoid and seek the maximum guarantee. It’s almost impossible to guarantee that there are no 100% exceptions due to the presence of asynchronous exceptions, but even implementing a strict guarantee in many cases can be unnecessarily expensive.
To conclude, we can say the following:
security guarantees for exceptions are not a panacea, but an excellent foundation for building robust applications .
----------------------
(*) In fact, the “hot” debate, in general, was not for one simple reason: you cannot program on the .Net platform without exception handling. Such debates are relevant, for example, in the C ++ language, especially when it comes to low-level programming.
(**) In general, no one ever requires the invariant to be preserved; usually it is necessary to preserve the invariant “before” and “after” the call of the
open method, but it is not necessary to save it after calling the
private method that performs only a “part” of the work.
(***) It may seem quite amusing that a more “tricked” language such as C # may not do something that old C ++ does, but it really is. As an example, let's rewrite the code discussed earlier with C # to C ++:
class Resource1
{
public :
Resource1()
{
// , -
//
}
~Resource1()
{
//
}
};
class Resource2
{
public :
Resource2()
{
// resource1_
throw std::exception( "Yahoo!" );
}
private :
Resource1 resource1_;
};
// - Resource2
Resource2 resource2;
* This source code was highlighted with Source Code Highlighter .
As mentioned earlier in C ++ (as opposed to C #), when an exception is generated, the destructors of already constructed fields (i.e., sub-objects) will be called automatically in the class constructor. This means that in this case the call to the destructor of the
Resource1 object will occur automatically and there will be no resource leaks.
Such differences in the behavior of C # and C ++ languages ​​are easily explained. In C ++, a resource is everything, including dynamically allocated memory, so resource management tools are at a higher level. An application programmer working with the C # language uses resources in the using block much more often than it seizes resources in the constructor. And if he faces such a task, he will have to solve it on his own, without the help of the compiler.
By the way, the Coat of Arms Sutter already told about it once in his article:
“Constructor Exceptions in C ++, C #, and Java” .
(****) Maybe I already got hold of these notes, but this is quite important and, it seems, the last but one. Such an example is often loved to ask at interviews, so now, my readers know the correct answer to it!
(*****) Everything said in this article applies only to synchronous exceptions, since it is almost impossible to guarantee consistent asynchronous exceptions such as
OutOfMemoryException or
ThreadAbortException . Behind the proof here: "
On the dangers of the Thread.Abort method. "