by Andrew Shapira

View Source

Introduction

Two ubiquitous coding strategies are to use assertions and to generate debugging output. The .NET System.Diagnostics.Debug and System.Diagnostics.trace classes are designed to help programmers do these things. Unfortunately, the organization of these classes makes using them inappropriate in many cases. This article describes the reasoning behind this conclusion and gives a solution that does not have the problems of the Debug and trace classes. C# code is included. The solution involves four entities: a class that is used for assertions and only assertions, a preprocessor token that controls whether the class executes assertions, and a similar class and preprocessor token for development output. We also give a general introduction to using assertions.

Outline

An outline of this article is as follows. The next two sections respectively introduce assertions and development output. These introductions are followed by a discussion of some goals that should be met by classes that assist with assertions and development output. The next section discusses the .NET Debug and trace classes and shows how these classes do not meet the goals. The remainder of the article presents two standalone classes that can be used to meet the goals.

Introduction to Assertions

Assertions are a traditional software engineering tool that can be used with almost all programming languages. In his excellent book, “Large-Scale C++ Software Design” [2], John Lakos discusses using assertions in C and C++ (the text has been adapted slightly for this article):

The Standard C library provides a macro called assert (see assert.h) for guaranteeing that a given expression evaluates to a non-zero (true) value; otherwise an error message is printed and program execution is terminated. Assertions are convenient to use and are a powerful implementation-level documentation tool for developers. Assert statements are like active comments -- they not only make assumptions clear and precise, but if these assumptions are violated, they actually do something about it.
The use of assert statements can be an effective way to catch program logic errors at runtime, and yet they are easily filtered out of production code. Once development is complete, the runtime cost of these redundant tests for coding errors can be eliminated simply by defining the preprocessor symbol NDEBUG during compilation. Be sure, however, to remember that code placed in the assert itself will be omitted in the production version.

An assertion is best used to test a condition only when all of the following hold:

  • the condition should never be false if the code is correct,
  • the condition is not so trivial so as to obviously be always true, and
  • the condition is in some sense internal to a body of software.

Assertions should almost never be used to detect situations that arise during software's normal operation. For example, usually assertions should not be used to check for errors in a user's input. It may, however, make sense to use assertions to verify that a caller has already checked a user's input.

An “assertion failure” is said to occur when an assertion detects that its condition is false and takes appropriate action, such as throwing an exception. Since this is exactly what an assertion is supposed to do, the term “assertion failure” is something of a misnomer. Nevertheless, the term is standard, and is useful because it provides a name for an important situation.

Let's look at an example of using assertions. This example deals with a small C# program; the true value of assertions may not become apparent until one has used assertions with larger programs. With this in mind, then, consider the following small example. (In this example we assume that the Assert.Test method is defined elsewhere and performs a function similar to that of C's assert macro.)

 class Shift
 {
 
    /*
         This method returns a circular left shift of x's right 3 bits.
         If x is not between 0 and 7 inclusive, undefined behavior
         may result, and this undefined behavior may change over
         time and may depend on what algorithm this method uses.
     */
 
    static int shift3(int x) {
         Assert.Test((x >= 0) && (x <= 7));
 
        return ((x >> 2) & 1) | ((x << 1) & 6);
     }
 
 
    static void Main() {
 
        while (true) {
 
            string s = Console.ReadLine();
 
            if ((s == null) || (s.Length == 0))   // no more input
 
               {  break ;  }
            
char c = s[0];
#if
inappropriate
             Assert.Test((c >= '0') && (c <= '7'));
             int x = c;
             Console.WriteLine("The result is {0}", shift3(x));
#else
             if
(((c >= '0') && (c <= '7'))) {
                 int x = c;
                 Console.WriteLine("The result is {0}", shift3(x));
             } 
else  {
                 Console.WriteLine( "Please enter a number between 0 and 7.");
             }
#endif
 
        }
     }
 }

The assertion in Main is inappropriate, because users may enter numbers outside of the range 0 to 7, and the program's normal function includes detecting such entries and responding appropriately. The code inside the #else region checks the input appropriately. This example illustrates a general test that weeds out some inappropriate assertions: ask whether the program would function correctly with a given assertion removed, and if the answer is “no,” then the assertion is probably inappropriate.

Next, let's consider the assertion in the shift3 method. This assertion is appropriate because shift3 explicitly assumes that its argument x is between 0 and 7. The documentation before shift3 implies that if the program is correct, then the calling method will ensure that the argument to shift3 is between 0 and 7, as does the code in Main's #else region. Were it shift3's responsibility to check its argument and return an error code for invalid values, then the assertion in shift3 would not be appropriate. Notice that the appropriateness of this assertion depends not only on the code but also on the documentation, i.e., on policies regarding the responsibilities of code.

The Shift class contains a serious bug. When we run the program, we find that the assertion in shift3 throws an exception. How can we find out what's going on? Well, the assertion did its job by throwing an exception, so we know immediately that the contract between shift3 and its caller has been broken. We can proceed by determining why the contract was broken, i.e., why shift3 received an improper value of x. The problem is that x in Main should be assigned the value c - '0', not c. After changing the assignment and recompiling we find that the program works.

The assertion in this example performs two valuable functions. First, it concisely summarizes the contract that shift3 has with its callers. The assertion makes it easy for a reader to quickly understand details of this contract. Second, if the contract is broken, the breaking of the contract is detected immediately. It is almost always easier to figure out what is wrong when a problem is exposed immediately, before program execution has reached a later point that may be only tenuously related to the source of the problem. This service that assertions can provide -- immediate detection of errors -- is called “feedback at the point of failure.”

More information about assertions can be found in web search engines and in software engineering textbooks.

Introduction to Development Output

We define development output to simply be program output that is intended to be generated only during the development phase of software production. Such output is often used for determining what is going on in a program, especially during debugging.

For example, while debugging the code in the previous section, we might want to print some output. We could do this by adding Console.WriteLine calls, as follows. (The code differs slightly from that in the previous section.)

static void Main() {
    while (true) {
        string s = Console.ReadLine();
        if ((s == null) || (s.Length == 0))
           {  break ;  }
        int
x = s[0];
        Console.WriteLine("s={0}", s);
        Console.WriteLine("x={0}", x);
        if (((x >= 0) && (x <= 7))) {
            Console.WriteLine("The result is {0}", shift3(x));
        }  else  {
            Console.WriteLine( "Please enter a number between 0 and 7.");
        }
    }
}

After running the program and seeing the development output, we may realize that the value of x is not being computed correctly from the string s. This may help us understand that we need to subtract the character constant '0' from s[0] when computing x. After fixing the bug and inspecting the development output in the fixed version of the program, we would likely remove the Console.WriteLine statements that produce development output.

Using the Console.WriteLine method like this to produce development output is not too bad. In fact, in a small program, sometimes this is the best way. This technique does have some drawbacks, though, and these drawbacks become important in large projects. First, it can be difficult to distinguish between temporary and permanent Console.WriteLine calls. Second, temporary calls like the ones in the example can come to reside in a program for a long time, possibly permanently, and we do not want these calls to produce output in released software. What we would like is a way for this output to appear during the development process, but not with released versions of software.

Goals for Providers of Assertion and Development Output Services

Let's look at the capabilities that we do and do not want from software that provides assertion and development output utility services. In particular we will look at what types of software builds should have assertions execute, and what types of builds shouldn't. We will also look at the same topic for development output.

Usually, we want assertions on (executing) during development. It's also useful to be able to turn assertions off during development, e.g., when code is temporarily structured in a way that causes assertions to fail.

In release builds, we usually want assertions off. But for some release builds it makes sense to have assertions on, especially when developers have a close relationship to the environment in which the released product is being used, or when developers run with assertions on all the time, as many developers do.

We also want to be able to turn development output on and off during development builds. Executables built in release builds should not produce development output.

Our goals, then, are as follows: we want a choice about having assertions on or off for development builds, a choice about having assertions on or off for release builds, and a choice about having development output on or off for development builds. We want development output to be always be off for release builds.

Deficiencies of the .NET Debug and trace Classes

Two obvious candidates for achieving our goals are the Debug and trace classes in .NET's System.Diagnostics namespace. Let's examine these classes to see if they help us meet our goals.

First, let's look at using the Debug class. We will call using the Debug class “Policy 1”; variants are called “Policy 1A”, “Policy 1B”, etc.

Policy 1A. Use Debug for both assertions and development output.

Policy 1A fails to meet our goals because it requires that if assertions are on in a given release build, then development output will also be on in the same build. This is because, as controlled by the DEBUG preprocessor token, either all the members of the Debug class are on, or none are.

Policy 1B. Use Debug for assertions, and mandate not using the members of Debug that involve development output.

One flaw with Policy 1B is that if we want assertions on in a release build, we have to define DEBUG for the release build, which is confusing at best. Also, this policy is difficult to maintain. Someone may simply forget to avoid using development output aspects of Debug. Or, when someone new to a project encounters code that uses Debug.Assert, she may start using the non-assert members of Debug because she may be used to this from other projects, or because when she sees Debug.Assert in the code, it may seem natural to use other parts of Debug.

Policy 1C. Use Debug for development output, and do not use the members of Debug that involve assertions.

Policy 1C's problems are essentially the same as Policy 1B's.

If one gives sufficient weight to the flaws described above, as we do, then one can conclude that the Debug class should be used for neither assertions nor development output.

Now let's look at using .NET's trace class.

Using the trace class has exactly the same problems as using the Debug class.

There is another problem with using the trace class. Grimes[1] has observed that “Visual Studio.NET defines trACE [the preprocessor token] for C# projects created with the project wizards.” This creates an expectation among Visual Studio.NET users that trACE will be defined for release builds. Respecting this expectation would mean that

  • if trACE controls development output, then release builds would contain development output, and
  • if trACE controls assertions, then we could not turn assertions off for release builds.
Both of these consequences violate the design goals in the previous section.

As with the Debug class, we conclude that the trace class should be used for neither assertions nor for development output.

The problems we have discussed with the .NET base class library's Debug and trace classes stem from the dependencies they introduce between assertions and development output. It is probably better to separate assert functionality from development output functionality. For example, the Debug and trace classes might better have been designed to have no assert functionality, and assert functionality could have been implemented in a separate class that is used for only assertions.

Our Solution

Since .NET's base class library does not provide a separate class that is used only for assertions, we developed our own system for assertions and development output. This system is very simple to understand and use. It comprises four entities:

  • the Assert class,
  • the Nib class,
  • the ASSERT preprocessor token, and
  • the NIB preprocessor token.

The Assert class is for assertions, and only assertions. The Nib class is for development output, and only development output.

Listing 1 shows the Assert class. The Assert class's public methods execute only when the ASSERT preprocessor token is defined. This is the only thing controlled by the ASSERT token.

Listing 2 shows the Nib class. The Nib class's public methods execute only when the NIB preprocessor token is defined, and this is the only thing controlled by the NIB token.

Below is an example where the Shift class has been written to use the Assert and Nib classes. (For the comments, see the previous examples.)

 class Shift
 {
 
   static int shift3(int x) {
         Assert.Test((x >= 0) && (x <= 7));
 
        return ((x >> 2) & 1) | ((x << 1) & 6);
     }
 
 
    static void Main() {
 
        while (true) {
 
            string s = Console.ReadLine();
 
            if ((s == null) || (s.Length == 0))  
 
               {  break ;  }
            
char c = s[0];
             if
(((c >= '0') && (c <= '7'))) {
                 int x = c;
                 Nib.WriteLine("s={0}", s);
                 Nib.WriteLine("c={0}", c);
                 Nib.WriteLine("x={0}", x);
                 Console.WriteLine("The result is {0}", shift3(x));
             } 
else  {
                 Console.WriteLine( "Please enter a number between 0 and 7.");
             }

          }
     }
 }

The use here of the Assert and Nib classes should be self-explanatory.

It's helpful to have a software engineering term that means, “something that controls development output.” In this regard, the term “nib” possesses the advantage of not having other meanings commonly associated with programming. Another nice feature is that that “nib” is only a few characters long. A bonus is that the meaning of the English word “nib” is related to the Nib class's function -- the nib of a pen is the part that applies ink to paper.

Conclusion

In this article, we have introduced assertions and development output, discussed goals for a provider of assertion and development output services, and reviewed problems with using the .NET Debug and trace classes for assertions and development output. We then presented a simple and easily understood system for using assertions and development output. This system uses two classes and two preprocessor tokens. We have found that this system is convenient and effective in practice.

References

[1] Richard Grimes, Developing Applications with Visual Studio.NET. Addison-Wesley, 2002, ISBN 0-201-70852-3.
[2] John Lakos, Large-Scale C++ Software Design. Addison-Wesley, 1996, ISBN 0-201-63362-0.

Listings

Listing 1: The Assert Class.

 using System;
 
 
class Assert
 {
 
    // Probably, FailedException instances should be created
     // only from within the Assert class.
 
    public class FailedException : ApplicationException {
 
        public FailedException(string s) : base(s) {}
     }
 
     [System.Diagnostics.Conditional("ASSERT")]
 
    public static void Test(bool condition)
     {
 
        if (condition)   { return; }
 
        throw new FailedException("Assertion failed.");
     }
 
     [System.Diagnostics.Conditional("ASSERT")]
 
    public static void Test(bool condition, string message)
     {
 
        if (condition)   { return; }
 
        throw new FailedException("Assertion '" + message + "' failed.");
     }
 }

Listing 2: The Nib Class.

 // (This version of the Nib class always writes to System.Console. Later
 // versions might add functionality similar to System.Debug.Listeners.)
 
using System;
 

 class
Nib
 {
     [System.Diagnostics.Conditional("NIB")]
 
    public static void Write(object obj)
     {
         Console.Write(obj.ToString());
         Console.Out.Flush();
     }
 
     [System.Diagnostics.Conditional("NIB")]
 
    public static void W(object obj) // short name
 
    {
         Console.Write(obj.ToString());
         Console.Out.Flush();
     }
 
     [System.Diagnostics.Conditional("NIB")]
 
    public static void Write(string s, params object[] args)
     {
         Console.Write(s, args);
         Console.Out.Flush();
     }
 
     [System.Diagnostics.Conditional("NIB")]
 
    public static void W(string s, params object[] args) // short name
 
    {
         Console.Write(s, args);
         Console.Out.Flush();
     }
 
     [System.Diagnostics.Conditional("NIB")]
 
    public static void WriteLine(string s, params object[] args)
     {
         Console.WriteLine(s, args);
         Console.Out.Flush();
     }
 
     [System.Diagnostics.Conditional("NIB")]
 
    public static void WL(string s, params object[] args) // short name
 
    {
         Console.WriteLine(s, args);
         Console.Out.Flush();
     }
 }

Revisions

4/17/04 - Original

About the Author

Andrew Shapira has been writing programs since 1976, after he was introduced to the PLATO computer system. He has worked in theoretical and applied computer science, computer engineering, large software projects, mathematics, and online games. Andrew received the PhD in computer engineering in 1997 from Rensselaer Polytechnic Institute. He lives in the Seattle area.

Link

View Source