Enhanced Assertions
--By andrei alexandrescu and john torjo
Liu Weipeng (PONGBA)
- This article with John Torjo describes a characteristic, industrial strength ASSERTION facility. The characteristics of this kit include multiple debug levels, logging, and a method of collecting detail status information.
Ok, I admit: I am experiencing "Writer's Block". The materials are here, cool. I have my favorite breakfast (Homemade Milk Assorted Breakfast, my private formula: 50% oatmeal, 50% nuts, 50% raisins - try to kill them), I trusted the open source editor being moving Flashing (I know what you are thinking: "Movie"? Funny, however, I can't come up with a good introduction. In order to alleviate my anxiety, dear readers, forgive me for using these funny words in the beginning of the article. However, this is a meta-programming column, isn't it? Coupled with you is a C programmer, so you have some additional syntax almost.
Since we put the first paragraph to one side, please let me introduce you to my friend John Torjo, C expert, consultant, "Practical C " column writer (http://buider.com/), we have long been From e-mail. John and I took this article, describing the improved assertion framework of John.
John reads my article about Assertions [1] (which is mostly Jason Shirk and the results I discussed), found that it has been lacking. More accurately, he found that he would need an ASSERTION facility to provide more features. All this is because I use the SIMPLE World Assumption environment for the article about Assertions and attached to the ASSERTIONS. This is a very professional term, you may also have not heard it, so please let me say it in detail.
Under Simple World Assumption, programmers have normal working hours and have reasonable schedules. They have time and encouraged code evaluation. Therefore, any failed assertions in the code will recruit a series of criticisms in one piece and overall test. The programmer tests and debug code, ensuring that there is no Assertion failed in all test environments, and finally compiles and sends a small and fast executable to their project manager in the case of NDebug macros, then the project manager will change it. Send to the appropriate consumer base group. By the way, under Simple World Assumption, the project manager is considered to have the ability to help programmers complete their work and do not give them pressure and burdens. (As it is exhibited, Simple World Assumption does not exist in reality)
In a more real world, the programmer has to endure the considerable pressure brought by the schedule, so the test of the single piece is replaced to the Black-Box directly without how to test. Test group, while the latter writes a bug report.
It is pointed out that the event sequence that will develop to bug is not always simple. In a variety of circumstances, the programming of the event drives makes the reproduction of the bug more difficult. The random behavior generated by the uninitialized variable, the error transformation, or the buffer overflow is only added some seasonings. Cough, I almost forgotten a wide range of system configurations, such as the installed DLL and registry settings ... have you been in your system perfectly running in another mysterious failed app?) A method that is helpful to this situation, John said it is to design a better Assertion Framework to extend the ASSERTION's ability. Clearly as follows:
* There is a multiple level of Assertions, and the design error is more obvious than white paper black characters. In an extreme, there is the most Checks, while the software's maturity will fail. In the other extreme, there is "low cost, high-efficiency" Checks, you can make them professional testers, beta version testers, sometimes or even reserved for your software's end users. I personally don't like to let the end users see Assertion Message, but John makes me believe that this is likely to happen. In addition, continue to read, because failure of Check can be reported in a variety of ways.
* Only the message is not enough, especially when the development and test group are working separately. A log record is extremely useful, so that after a certain run fails, the developer can see which assertion (or which Assertions) failed.
* The quality of the message is greatly improved, including not only the details of the failed expressions, files, rows, but also the values of the associated variables (under the control of programmers).
All of these additional features have formed an industrial intensity Assertion kit, on the original assertion code, John added some skills used in my article "Enforcements", and many of them belong to himself. Skills. We will introduce how to work in the following detailed ASSERTION toolkit.
Provide additional status information (providing extra state information)
When an assertion fails, the Assert expression is evaluated to be false. However, you might often want to know which key variables are the failure of Assertion. For example, considering the time when you believe two String is empty, you write the following code:
String S1, S2;
...
(S1.empty () && s2.empty ());
If this Assertion fails, you are likely to know what is keys, such as STRING objects, such as S1 or S2. John's Framework allows you to do this with the following syntax:
Smart_assert (s1.empty () && s2.empty ()) (S1) (S2);
It is noted that the use of parentheses provides an additional parameter in the use of additional parameters, and this is similar to Enforce [2]. (Of course, Enforce is not the first component of Operator () in this special style)
When doing this, if the assertion failed, the message displayed and recorded as a log looks like this:
Assertion Failed in Matrix.cpp: 879412
Expression: 's1.empty () && s2.empty ()'
VALUES: S1 = "Wake Up, NEO"
S2 = "It's time to reload." This is how magical work. Preparing to accept some trick but it is undoubtedly something worth learning. (In order to be able to understand and use, you need to wake your heart to the heart to the macro love) First, the basic idea is to get the value of the variable name and variable, you need to use "String In Operator #" (Translation: A special The operator can make the text behind it into a C / C string form, such as #class is replaced by the compiler to "Class"), in the above example, you need to use String In Operator to MyString. But it can only be used inside the macro, and the following facts make things tricky: you need a macro machine that can "unlimited extensions" - an extension macro still can continue as a macro (so you can Collect more variables S3, S4 ... information). But we all know that this recursion macro does not work.
However, if you can open the car roof while stepped on the throttle and raise an antenna with your left hand, you will find that this skill is difficult but it is feasible. Honor belongs to Paul Mensonides, which is invented by him (as we are known). Here is what you need to do:
First, add two member variables smart_assert_a and smart_assert_b to your Assert class (this class and the same name defined and used by the same name in the previous article). Their type is ASSERT &.
Class assert
{
...
PUBLIC:
AskERT & SMART_ASSERT_A;
AskERT & SMART_ASSERT_B;
// Whatver Member Functions
Assert & Print_Current_Val (Bool, Const Char *);
...
}
Make sure you use these members to initialize. All of these intentions are: If you have an ASSERT type object Obj, you can write obj.smart_assert_a or obj.smart_assert_b, while their behavior is exactly the behavior of OBJ itself, because they are references to * THIS. Second - Cool Skills will be on -tendent - define two macros: smart_assert_a and smart_assert_b, they "recursively to another" in some way, like this:
#define smart_assert_a (x) smart_assert_op (x, b)
#define smart_assert_b (x) smart_assert_op (x, a)
#define smart_assert_op (x, next) /
Smart_assert_a.print_current_val ((x), # x) .smart_assert _ ## Next
As you can see, when you call smart_assert_a (xyz), it will be extended into a code ending with smart_assert_b. When you call Smart_ASSERT_B (XYZ), it will be extended into a code ended with smart_assert_a. When expanding, the two macros get the value of the value of the value of XYZ and the value you passed (the translation: "XYZ", which is implemented by StringIzing Operator.).
Yes, this is really tricky. A view that can help you understand this skill is: When the pre-processor sees smart_assert_a (or _b), it will treat this as a macro. If there is no parentheses, the preprocessor simply retains this symbol to it. In the latter case, the symbol smart_assert_a (or _b) is only representing the member variable. Here is a smart_assert macro definition, starting the two macros (smart_assert_a and smart_assert_b) Turning Action: #define smart_assert (expr) /
IF ((expr)); /
Else make_assert (#expr) .print_context (__ file __, __ line __). Smart_assert_a
If you say "AHA!" At this time, then your opinion is the same as me in reading these code twenty times. Ok, now let us start tracking macros from the initial expression:
Smart_assert (s1.empty () && s2.empty ()) (S1) (S2);
Let us first expand the smart_assert macro. (We don't have to start according to the order of the preprocessor, in order to make the process clearly, we divide the process into several blocks. We will also adjust the format of the unfolded code)
IF ((s1.empty () && s2.empty ()));
Else make_assert ("s1.empty () && s2.empty ()").
Print_Context ("Matrix.cpp", 879412) .SMART_ASSERT_A (S1) (S2);
Now let's start smart_assert_a macro, as well as smart_assert_op macros after the expansion:
IF ((s1.empty () && s2.empty ()));
Else make_assert ("s1.empty () && s2.empty ()").
Print_Context ("Matrix.cpp", 879412).
Smart_assert_a.print_current_val ((S1), "S1").
Smart_assert_b (s2);
Note How Smart_ASSERT_A is no longer treated as a macro, that is because it is not followed behind ((). The end result after the expansion smart_assert_b and smart_assert_op is:
IF ((s1.empty () && s2.empty ()));
Else make_assert ("s1.empty () && s2.empty ()").
Print_Context ("Matrix.cpp", 879412).
Smart_assert_a.print_current_val ((S1), "S1").
Smart_assert_a.print_current_val ((S2), "S2"). Smart_assert_a;
Considering that SMART_ASSERT_A is treated as a member variable, and each member function returns a reference to Assert, this is a perfect formation.
Failure processing and logging (HANDLING AND Logging)
When an assertion fails, there are two things happenful:
* Failure information is recorded as a log
* Failure is appropriately processed according to its level
These two movements are completely straight and can be customized separately. For example, you can be in such a mode: you never ask for a record error, but you still record all the errors, this is automatically run, Push Installation (Encourage installation? Force installation?), Or your "Innocent User Protection Program" It is useful for programs that are not known to provide protection. You can customize logging by passing your own log record (logger) to static member functions assert :: set_log (const assert_context &)). You can define your own failed handling routines and place it by calling assert :: set_handler (const assert_context &)) (this is the same as the originate set_unexpected style). Assert_Context contains the context (acquired context obtained from the failed assert), which will be explained below.
Not only one assert (Not Just One Type of Assert)
As some old programmers noted, an app can have different Assert levels, some of which are more important than others.
Let's take a look at how Assert is used. Typically, when you assume that some cases never happen, you will use assert (for example, you expect a size or an index variable never negative).
Sometimes you combine Assertion with some kind of defense programming style (such a code written "survive" in the case of certain invalid inputs), but not always like this. In the latter case, throw an exception will be better. Take a look at the following code:
Void Install_Program (user & user, const char * program_name) {
// only admin s can install program
Assert (user.get_role () == "admin");
...
}
Assertion has four levels:
* LVL_WARN (just a warning, if no user interfering program can continue)
* LVL_DEBUG (default, usual assert)
* LVL_ERROR (an error)
* lvl_fatal (this is a fatal error, very likely that the program or system has become unstable)
Each level can be handled in different ways. Therefore, our default processing of each level is as follows:
* Warning: Dumps (dump) messages and procedures continue.
* Debug: What is the user to do (ignore? Debug? ...).
* Error: Throw an exception (std :: runtime_ERROR).
* Fatal: Terminate the program.
You can rely on default, or, as mentioned above, changing processing routines, very simple, like this:
AskERT :: set_handler (lvl_error, my_handler);
The following teaches you how you set the level of Assertion:
Smart_assert (user.get_role () == "admin"). Level (LVL_ERROR);
Smart_assert (user.get_role () == "admin"). Level (lvl_debug); // This is the default
Smart_assert (user.get_role () == "admin"). Level (lvl_fatal);
Smart_assert (user.get_role () == "admin"). Level (lvl_warn); // More simple way
Smart_assert (user.get_role () == "admin"). Error ();
Smart_assert (user.get_role () == "admin"). Debug ();
Smart_assert (user.get_role () == "admin"). Fatal ();
Smart_assert (user.get_role () == "admin"). Warn ();
Get context (acquiring context)
How to deal with you when an assertion fails. For example, you can decide to ignore it (if it is just a Warning), or throws an exception, or makes other similar things. But the most important thing is that if you decide to display it, you can choose how the data is displayed and which data will be displayed. You may choose to display only a message and provide a "advanced" option similar to Figure A.
You can customize log records in a similar style. To allow this, when an assert is fails, it gets the context: file name, line number, level, and evaluate the expression of false, and the variable value associated with the expression. Get __file__ and __line__ methods are similar to those described in [1].
Class assert_context {
PUBLIC:
// Where the assertion failed: File & Line
Std :: string get_context_file () const;
INT GET_CONTEXT_LINE () Const;
// Get / set expression
Void set_expr (const std :: string & str);
Const std :: string & get_expr () const;
Typedef st :: pair
Typedef st :: vector
// Return Values Array As a Vector of Pairs
// [Value, Corresponding String]
Const Vals_Array & Get_vals_Array () Const;
// adds one value and its corresponding string
Int Add_val (Const std :: string & val, const st :: string & STR);
// Get / set level of assertion
Void Set_level (Int Nlevel);
INT GET_LEVEL () Const;
// Get / set (user-friendly) message
Void set_level_msg (const char * strmsg);
Const std :: string & get_level_msg () const;
}
Usually, when logging a log, you want to record as much information as possible. Therefore, most of you will be very satisfied with the default log record routines, it writes all things in Context. Failure processing is another extreme. There is an unlimited number of methods for processing an Assert.
* Ignore it just.
* Display a summary for users (related file names, line numbers, and expressions).
* Display all details in the Console window. * Throw an exception.
* Display a summary or detail in a UI dialog.
* Terminate and perform core dumps, and so on.
When your application is in the beta phase, you will feel happy. However, as your program matures and has a lot of users, you will want to get the final control. You almost certainly want to replace the default process routines and provide yourself.
A simple look like this:
/ / Display a message box with two buttons with "ignore" and "ignore all"
Void CustomerFriendly_Handler (const assert_context & ctx) {
Static Bool ignore_all = false;
IF (ignore_all) return;
Std :: ostringstream out;
IF (ctx.msg (). size ()> 0) OUT << msg ();
Else Out << "Expression: '" << ctx.get_expr () << "' failed!";
Int result = message_box (out.str ());
IF (Result == Do_IGNORE_ALL)
Ignore_all = true;
}
// Place it to place it
Assert :: set_handler (LVL_Debug, customer_handler);
For user-friendly message
As mentioned in the introduction, you don't always debug your procedure yourself. Old qualified programmers always tell you to provide documentation for your code. Like ASSERT. When an assertion fails, you want to know what it means. Let us look at a behavioral friendly Assert:
// Too Many Users!
ASSERT (NUSERS <1000);
If this assert is failed, the message that is about "NUSERS <1000" will be displayed and recorded. Then, ahead will be a string that allows you to show your own interpretative, which will make the expression more eloquence:
Smart_assert (NUSERS <1000) (NUSERS) .MSG ("Too Many Users!");
This is a quite elegant solution. Because it makes code more "self-explan" (self-document ", the intent of the code itself can be seen from the code itself), and makes the error message impostful.
The MSG () member function does not change the ASSERTION level. The Level () member function sets the ASSERTION level, and you can also provide an optional message string. These auxiliary functions are used to change the ASSERTION level to Warn, Debug, Error, Fatal and simultaneously keep up with a message string. as follows:
// using level ()
Smart_assert (NUSERS <1000) (NUSERS) .LEVEL (LVL_Debug, "TOO MANY USERS!");
Smart_assert (NUSERS <1000) (NUSERS) .LEVEL (LVL_ERROR, "TOO MANY USERS!");
// using helpers
Smart_assert (NUSERS <= 900) (NUSERS) .warn ("Users Aproaching Max!");
Smart_assert (NUSERS <1000) (NUSERS). DEBUG ("Too Many Users!");
Smart_assert (NUSERS <1000) ("Too Many Users!"); Smart_assert (NUSERS <1000) .fatal ("Too Many Users!");
Refreshable (Ignorance is bliss)
In case you are dealing with the code of others (sometimes these code is not changed), and there is an Assertion to fail, you will be happy to learn about the existing "Ignore Always" option. "Ignore Always" is as shown in [1], but it uses other implementations after the scene: Remember all the file names and line numbers corresponding to the failed assertion of the "ignore always".
This mechanism assumes that you will not have two smart_asserts in the same line. Its advantage is interesting "Levels of Gran Guity" - just like "Ignore All Assertions In File Xyz.cpp" ---- now has become possible.
"Ignore All" is useful when using your program in a non-service. In case there are many Assertions failed in the same line, users prefer to use the "Ignore all" option. Also, the tester can use "Ignore All" to speed up the test speed because they know that Assertions will be recorded.
You can define your own processing strategy. For example, John's full implementation of Smart_assert [3] defines a two-way maintenance mechanism that makes "Ignore Always" and "IGNORE ALL" always work in continuous running in your program. That is quite elegant.
What is the release mode? (What about the release mode)
The traditional way using Assert is to use it as a debug tool. The intensity of Assert originated from the NO-COST's promise - they do not exist in the Release version (in Release mode, they have never existed there - there is no additional burden).
However, this is not always the best way. Sometimes the user wants your program for a Release version (in efficiency considerations). In Release mode, all Asserts disappeared, so BUGS has become difficult to track. During the early development, you would want to keep Assertions in the Release version. Even Windows NT has a Debug version to make it easier for programmers.
You should also notice in debug mode, not necessarily assert slow down the speed of the program, and more like the compiler Flags indicates that it should not optimize the code. So in many cases preserves Assert in Release mode (optimized) may not be slowed down.
Use Smart_ASSERT to make smart_assert_stert_debug_mode macro, it is easy to turn smart_assert to or off. If you don't define it, the default is: smart_assert exists in debug mode, disappears in Release mode.
If you choose to define it, here is what you need to do:
#define smart_assert_debug_mode 0 // Smart_asserts Are Off (gone)
#define smart_assert_debug_mode 1 // Smart_asserts Are ON (Present)
Finally, there are also Asserts you exist in Release mode, even if they may cause some (small) extra burden. These are usually the most critical and important parts of your code. Smart_verify behavior is similar to smart_assert, but there are two points: smart_verify works in debug mode and Release mode, smart_verify's default level is Error (LVL_ERROR), but smart_assert's default level is Debug (LVL_Debug). This is an expectation, because if a smart_verify fails, the program continues to execute according to the normal path, the program is likely to crash (therefore, an exception will also be thrown). Here is an example: widget * p = get_nth_widget (n);
Smart_verify (p) (n) .msg ("Widget Does Not Exist";
// if p bere null, and we reached this point, the program 10 Most LIKELY CRASH
P-> Print_Widget_info ();
Conclusion
John's work created a fully characteristic, industrial intensity ASSERTION facility. As before, using Assertion is still a key factor for successful programs (and successful programmers). It's better to change now, we have this facility, which makes you define, use, and analyze the invariant process for your program. Happy asserting!
Acknowledgements
I am very grateful to Pavel Vozenilek, who encourages John to write this library from the beginning. Thank Paul Mensonides to provide code that allows "chaining" macro. Thanks to the Boost Committee to test the code for John and give many affirmative responses.
Reference Bibliography and Note (Bibliography and Notes)
[1] Andrei Alexandrescu. "Assertions (http://www.moderncppdesign.com/publications/cuj-04-2003.html)".
[2] Andrei Alexandrescu and Petru Marginean. "Enforcements (http://www.moderncppdesign.com/publications/cuj-06-2003.html)".
[3] http://www.torjo.com/smart_assert.zip
About the authors
Andrei Alexandrescu is a Ph.D. of the University of Washington, Washington, and the author of the book book of Modern C Design. You can contact him via http://www.moderncppdesign.com/. Andrei is also one of the appeal of the C seminar.
John Torjo is a free writer and a C consultant. He loves C , generic programming, stream. He also likes to write articles, you can contact him through John@torjo.com.