The Database Managers, Inc.

Contact The Database Managers, Inc.


Use an RSS enabled news reader to read these articles.Use an RSS enabled news reader to read these articles.

Bullseye Error Reporting

by Curtis Krauskopf

One of the challenges in reporting runtime errors is providing enough information to find where the error originates.

Wouldn't it be nice if there was a way to unambiguously report the exact line that an error occurs on? Wouldn't it be even nicer if the compiler would do all of the bookkeeping for you so that you could focus on the coding? Fortunately, there is a solution.

The Borland C++ Builder provides three predefined macros that provide information about the line being compiled: __FILE__, __LINE__ and __FUNC__.

The __FILE__ C++ macro contains the drive, path and name of the file being compiled. The __LINE__ macro contains the line number of the original source file that is being compiled. The __FUNC__ macro contains the name of the function being compiled. On other compilers, this might be called __FUNCTION__ [1] or __func__ [2].

All three of these macros have two underscores both before and after the word. Separating each character of the __FILE__ macro looks like:

_ _ F I L E _ _

Listing A shows Simple_Error_Report.cpp and Figure A shows its output. Simple_Error_Report.cpp is a console-mode application that shows an easy way to associate runtime errors with the original source line.

Listing A
// Simple_Error_Report.cpp
#include <stdio.h>

int main(int , char**)
{
  printf("Error in %s on line %d, in %s\n", __FILE__, __LINE__, __FUNC__);
  return 0;
}
hash.cpp sample program.

Figure A
Error in C:\Sandbox\Error_Location\Simple_Error_Report.cpp on line 5, in main
The output of Listing A shows the values of __FILE__, __LINE__ and __FUNC__

The result in Figure A shows some interesting information.

  • The __FILE__ macro expands to a filename with the drive and path of the original compilation unit.
  • The __LINE__macro is an integer, as evident from the %d printf() format specifier.
  • The __FUNC__ macro does not contain the return type or the parameter list of the function.

Taking It One Step Further

I want every error report to go through a common error() function so that I can set breakpoints when certain errors occur and so I can segregate how errors are handled. For example, I might want some errors to appear on the screen and other errors to be logged to a file. And some errors might need to appear on the screen and be logged to a file. Because the name of the function is available, I might want to take advantage of that by doing special processing based on the name of the function. For example, an error caused in a getter or setter will have a get_ or set_ prefix on the function name.

A prototype for such an error logging function is:

void error(const char *file, const unsigned long line, const char *name, const char *msg);

and it would be called like this:

error(__FILE__, __LINE__, __FUNC__, "my error message");

Preprocessor Magic

There are three awkward parts to the above solution:

  1. The __FILE__, __LINE__ and __FUNC__ macros need to be added to every error() function call.
  2. It's easy to forget to put both underscores on both parts of the __FILE__, __LINE__ and __FUNC__ macros. Getting it wrong will lead to a compile-time error.
  3. __LINE__ is an integer. Doing string manipulation on an integer just adds another complexity to any error() function I create. It is unlikely that I will ever use __LINE__ as an integer -- I always want to use it as a string so that it can be output to the screen or a log file.

It would be nicer if __FILE__, __LINE__ and __FUNC__ could somehow be handled automatically so I couldn't get them wrong every time I write an error() call.

What I want to write is something like:

error(AT, "my error message");

Using the program in Listing A as an example, the AT macro in that program would expand to be:

"C:\Sandbox\Error_Location\Simple_Error_Report.cpp:5:(main)"

The prototype for my new error() function becomes:

void error(const char *location, const char *msg);

Because the Borland C++ Builder compiler automatically merges adjacent strings, I can create a #define for AT that looks like this:

#define AT __FILE__ ":" __LINE__ ":(" __FUNC__ ")"

That doesn't work, though, because __LINE__ expands to an integer. The above #define expands to this at compile-time:

"c:\temp\test.cpp" ":" 5 ":(" "main" ")"

That is an invalid string because strings can't have an unquoted integer in the middle of the string.

A special preprocessor directive that turns a symbol into a string is the # token. Changing the above #define to

#define AT __FILE__ ":" #__LINE__ ":(" __FUNC__ ")"

seems like it should work but it doesn't because the compiler complains that # is an illegal character. The problem is that the # preprocessor symbol is only recognized when it's used like this:

#define symbol(X) #X

So, not being one to fight the problem, I'll create a macro called STRINGIFY and change my AT macro to look like this:

#define STRINGIFY(x) #x
#define AT __FILE__ ":" STRINGIFY(__LINE__) ":(" __FUNC__ ")"

These lines compile and the sample program in Listing B shows the test program.

Listing B
// Developing_Solution.cpp
// This solution does not work because the __LINE__ macro is *itself*
// part of the output!
#include <stdio.h>

#define STRINGIFY(x) #x
#define AT __FILE__ ":" STRINGIFY(__LINE__) ":(" __FUNC__ ")"

void error(const char *location, const char* msg)
{
  printf("Error: %s at %s\n", msg, location);
}

int main(int , char**)
{
  error(AT, "example");
  return 0;
}
Developing_Solution.cpp sample program.

Figure B
Error: example at c:\sandbox\Error_Location\Developing_Solution.cpp:__LINE__:(main)
The output of Listing B has an unexpected result.

As shown in Figure B, the __LINE__ preprocessor directive itself has become a part of the output!

The solution is to take the STRINGIFY() solution one step further -- to wrap the STRINGIFY() macro in yet another macro:

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define AT __FILE__ ":" TOSTRING(__LINE__) ":(" __FUNC__ ")"

The complete program is in listing C and its output is in figure C.

Listing C
// Better_Solution.cpp
// This solution provides a complete example of using __FILE__, __LINE__
// and __FUNC__ to report an error's location.
#include <stdio.h>

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define AT __FILE__ ":" TOSTRING(__LINE__) ":(" __FUNC__ ")"

void error(const char *location, const char* msg)
{
  printf("Error: %s at %s\n", msg, location);
}

int main(int , char**)
{
  error(AT, "example");
  return 0;
}
Better_Solution.cpp sample program.

Figure C
Error: example at C:\Sandbox\Error_Location\Better_Solution.cpp:17:(main)
Listing C gives an expected (although lengthy) result.

Too Much Information

The pendulum for this problem started at no debug information and now it has swung so far that I'm getting too much information. I really don't need, for most of my projects, to know the drive and the complete folder path to the source file. This is especially true for my projects that are nested five or more folder levels deep.

Because all of my error reports are going through the same error() function, I can easily customize the report by stripping the drive and path information at runtime.

If you don't want the drive and path information on the error report, then add a #include <string.h> and change the error() function to:

void error(const char *location, const char* msg)
{
  const char *no_path = strrchr(location, '\\');
  if (no_path)
    ++no_path;  // move off of '\\'
  else
    no_path = location;

  printf("Error: %s at %s\n", msg, no_path);
}

The sample code is available in Strip_Path.cpp in the .zip file. The output of the sample program is in Figure D.

Figure D
Error: example at Strip_Path.cpp:25:(main)
Figure D: Stripping the path information provides a more succinct error message.

As expected, the error message is a little bit easier to read. For small or medium-sized projects, removing the drive and folder path does not reduce your ability to find the location of the error.

Conclusion

The macros __FILE__, __LINE__ and __FUNC__ can provide some very useful debugging information. This information can be made available at runtime by print()ing those values to the screen or to a log file.

Transforming the __LINE__ macro into a string turned out to be much more difficult than originally imagined. Through the use of some #define preprocessor magic, though, the __LINE__ macro was tamed and forced to compile as a string.

This has the advantage that the string is automatically merged with the values of the __FILE__ and __FUNC__ macros to create one string for error processing. This also has the advantage of removing the need for integer to string conversion in the error() function and then merging the resulting strings at runtime.

[1]: GNU GCC Compilers before version 3 define __FUNCTION__ and all GNU GCC compilers starting at version 3 recognize both __FUNCTION__ and __func__.

[2]: __func__ is part of the C99 standard but the Borland C++ Builder compiler (at least as of version 6) does not recognize __func__ as a predefined symbol.

Contact Curtis at

Curtis Krauskopf is a software engineer and the president of The Database Managers (www.decompile.com). He has been writing code professionally for over 25 years. His prior projects include multiple web e-commerce applications, decompilers for the DataFlex language, aircraft simulators, an automated Y2K conversion program for over 3,000,000 compiled DataFlex programs, and inventory control projects. Curtis has spoken at many domestic and international DataFlex developer conferences and has been published in FlexLines Online, JavaPro Magazine, C/C++ Users Journal and C++ Builder Developer's Journal.


Popular C++ topics at The Database Managers:

Services | Programming | Contact Us | Recent Updates
Send feedback to: