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 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:
- The __FILE__, __LINE__ and __FUNC__ macros need
to be added to every error() function call.
- 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.
- __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) |
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) |
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) |
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:
|