Click here to Skip to main content
Click here to Skip to main content

Defining a unique macro receiving multiple arguments of any type

By , 28 Dec 2003
 

Introduction

One of the common use of C++ macros is to deal with functions like tracing or logging to a file. As these functions usually works with strings generated by dynamic data, passing multiple arguments to the macro without having to convert each one to a string may be useful, and sometimes the developer don't know the way to go. It can be made by a lot of ways, and the Code Project have similar articles on this topic but the solution I present here use a different approach that result in a clean syntax without character formatting like "%d" / "%s" / "%c"... a sample usage may be:

MYTRACE( “The username found is ”, strUserName,
         “ having ”, intAge,
         “ years old. The database key is ”, lngUserKey );

I'll use tracing as an example application, a real tracing class may consider issues like multithreading code protection and others that are out the scope of this article.

Background

Suppose you have to trace information in your code. Usually you'll encapsulate it in a class:

class MyTraceClass
{
   static void Trace( const string &message )
   {
      // trace code...
   }
};

As we need a easy way to disable the tracing code on release builds, we'll define a macro, like:

#define MYTRACE( x ) MyTraceClass::Trace( x )
It works fine, but this way we can pass just one string argument. Some programmers define various macro versions like TRACE / TRACE2 / TRACE3... one for each number of arguments, clearly it isn't an elegant solution because the macro user need to know the correct macro version accordingly the arguments being passed. Using va_arg the problem can be solved. The MFC defines the TRACE macro as:
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...);
.
.
.
#define TRACE  ::AfxTrace
So we can pass many arguments to the macro using the printf format specifiers like "%d" / "%s" / "%c"...
int i=10;
.
.
.
   TRACE( “Iteration number:  %d”, i );

The code above works fine but we have to be aware of the format specifiers, and we can't pass a non-LPCTSTR data as the first argument to TRACE without manually convert to LPCTSTR. As the tracing code will be "turned off", any aditional code is a overhead and will clutter the rest of code.

The proposed solution

Using templates and overloading, the macro can be called with a variable number of arguments of any type. The stringstream class helps to easily convert each argument to string, composing the final information.

First, the macro will be defined to the name of the static method:

#define MYTRACE  MyTraceClass::Trace

To receive arguments of any type, we'll define the tracing function receiving typename arguments:

template<typename T1> static void Trace( T1 par1 );

To receive multiple arguments, provide various versions of the overloaded method. Now the arguments can be converted to string using the stringstream class:

template<typename T1, typename T2> static void Trace( T1 par1, T2 par2 )
{
stringstream ss;
   ss << par1 << par2;
   Trace( ss.str() );
}

template<typename T1, typename T2, typename T3> 
  static void Trace( T1 par1, T2 par2, T3 par3 )
{
stringstream ss;
   ss << par1 << par2 << par3;
   Trace( ss.str() );
}

template<typename T1, typename T2, typename T3, typename T4> 
  static void Trace( T1 par1, T2 par2, T3 par3, T4 par4 )
{
stringstream ss;
   ss << par1 << par2 << par3 << par4;
   Trace( ss.str() );
}

.
.
.

This way we can call the macro with syntaxes like below. The screen illustrates a simple cout output.

MYTRACE( "Method Add() Called !" );
MYTRACE( "Iteration number: ", i );
MYTRACE( “The username found is ”, strUserName,
         “ having ”, intAge,
         “ years old. The database key is ”, lngUserKey );

Obviously the number of arguments can't exceed the maximum defined on overloaded methods, but it cannot be considered a problem, tracing / logging code usually don't need too much arguments, at worse it's easy to extend the class to support more overloaded methods.

This is my first article on Code Project. I hope someone might find it useful :)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

GuimaSun
Web Developer
Brazil Brazil
Member
I live in São Paulo - Brazil - where I have my tiny C++ \ Win32 \ .NET training company: NEXSUN
I have worked with many programming languages like VB / C++ / Java / C# but I´m mainly interested in: C++ forever Smile | :) and the .NET framework. Now I'm IBM-OOAD, OMG-OCUP, MCP, MCAD, MCSD, SCJP, MCSD.NET, MCTS, MCPD.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralATLTRACEmembernyc12322 Jan '04 - 9:34 
ATLTRACE, defined in <atltrace.h> takes variable args without a template.   Here are the key ingredients:
 
#define ATLTRACE ATL::CTraceFileAndLineInfo(__FILE__, __LINE__)
 
class CTraceFileAndLineInfo
{
public:
     CTraceFileAndLineInfo(const char *pszFileName, int nLineNo)
 
...
 
     void __cdecl operator()(const wchar_t *pszFmt, ...) const
     {
          va_list ptr; va_start(ptr, pszFmt);
          ATL::CTrace::s_trace.TraceV(m_pszFileName, m_nLineNo, atlTraceGeneral, 0, pszFmt, ptr);
          va_end(ptr);
     }
 
...
 
};
GeneralRe: ATLTRACEmemberGuimaSun2 May '04 - 4:59 
As you can see this is just the tradicional approach, using variant argument list (va_list), which will limit you to pass only string data types like LPTSTRs Wink | ;)
 
GuimaSun
www.nexsun.com.br
NEXSUN TechZone

Generalcool. and there is another waymembertimepalette3 Jan '04 - 18:47 
that is: create a class and implement operator <<.
 
overrided operator << can return (*this). so we can use a series of it in one line.
 
in func operator <<, we can dump msg to file, or to a richedit ctrl, or use it's parameters to change status or do some action. like
i(o)stream::operator<<.
 

GeneralRe: cool. and there is another waymemberkozlowski4 Jan '04 - 12:50 
But how to remove the expression like:
TRACE << "Text" << 1;
from Release compilation.
 
For TRACE("Text", 1) Microsoft use __noop compiler extension.

GeneralRe: cool. and there is another waymembertarzoon5 Jan '04 - 1:52 
How about:
 
[code]
if ( false )
debug_stream << ... << ...;
[/code]
GeneralRe: cool. and there is another waymemberkozlowski5 Jan '04 - 22:47 
Advantage of TRACE is that it is automatically removed by PROPROCESSOR from the code and it is NOT compiled.
 
Disadvatages of proposed solution are:
- nobody will use the if before debug_stream, because programmers are lazy,
- if somebody adds the if, he will write condition opposite for at least once per few uses,
- increased complexity of the code - additional if statement,
- increased size of Realease executable,
 
Consider that writing ASSERT macro in right way is not an easy task.
Writing correct TRACE macro is many times more difficult task.
 
My opinion is that Development Team of the compilers is group of very very skilled and experienced programmers, if it would've been reasonable to do the TRACE using stream they would certainly used streams. So the function like TRACE should be the best known solution for tracing in real-time debugging.
 
Greetz,
GeneralRe: cool. and there is another way - An alternativememberheyto6 Jan '04 - 4:36 

One alternative, that might provide the best of both worlds it to used two different types of streams and macro selecting between them. One providing useful logging, the other to (hopefully*) compile away to nothing.
 
*I can't check this works, as I don't have a professional copy of MSVC so can't turn on the required level of optimisation.
 
The following example tries to illustrate this uses a custom stream, which does nothing with it's input (NullOStream) and standard out (std::out).
 
If anyone's interested in a full blown article, then let me know and I'll expand on my flavour of a stream based logger (streams, filtering, stack dumping) But as there are already a number of good logging articles on CodeProject, so I've held back until now.
 
For anyone who's interested an implementation of my logger can be found at www.sourceforge.com/projects/pixel-commandos under src/logging
 
Cheers,
 
Tom
 

Disclaimer: I don't have a compiler in front of me, so this probably contains typos.
 
class NullOStream
{
  public:
  
   /**
     * Handle all value types, but do nothing with them.
     * (hopefully this in-line method will be optimised away)
     */
    template &lttypename T&gt
    NullOStream& operator &lt&lt (T /*p_Value*/)
    {
      return *this;
    }
 
    static NullOStream Instance;
};
  
  
// Add code to provide storage and initialisation
NullOStream NullOStream::Instance;
  
  
#ifdef NDEBUG
  #define TRACE NullOStream::Instance
#else
  #define TRACE std::cout
#endif
  
  
// usage
TRACE &lt&lt "hello world" &lt&lt aVariable &lt&lt 20;

GeneralRe: cool. and there is another way - An alternativememberkozlowski6 Jan '04 - 14:39 
I was surprised, but it really works for MSVC (.net Architect).
But, it doesn't work for function calls:
TRACE << rand();
 
I'm too lazy to install the VC6 environment to check it.
I suppose it will not work.
 
The old basic programming rule says:
"Never rely on compiler optimization" Wink | ;-)
 
You did not convinced me. I still see the same disadvantages I mentioned in previous post.
 
Greetz
GeneralRe: cool. and there is another way - An alternativememberheyto7 Jan '04 - 21:47 
kozlowski wrote:
But, it doesn't work for function calls:
TRACE << rand();

 
Fair point, though I guess we don't want to get into the argument of using TRACE with functions that have side effects Wink | ;) i.e. rand()
 
I've taken a quick look at __noop and can't get an example to compile under VC6, so maybe it's new in the VC7 (.net) However I was inspired by you question and came up with the following code - which is far simplier than the original post Smile | :)
 
#ifdef NDEBUG
  #define LOG(x)
#else
  #define LOG(x) (std::cout << x)
#endif
 
int func()
{
  std::cerr << "func called" << std::endl;
  return 30;
}
 
int main(int argc, char* argv[])
{
  LOG ("Hello World!:" << 20 << ',' << func() << std::endl);
  return 0;
}

GeneralRe: cool. and there is another way - An alternativememberkozlowski7 Jan '04 - 22:43 
Fair point because I wanted to show simple example.
Most programmers are lazy and doesn't use const to indicate the members of class without side effects. It's very likely that somebody will use the function with side effect inside TRACE.
i.e.
TRACE(DataStore.ComputeSomething());
I can agree that it is disadvantage of the TRACE, because Release code works differently than original.
 
About LOG let's try:
LOG(flags & 0xC5)
It is still far from reliability of the original TRACE Smile | :)
You see how difficult it is to define such macro, in really good way.
 
Greetz
GeneralRe: cool. and there is another way - An alternativememberheyto8 Jan '04 - 3:41 

kozlowski wrote:
About LOG let's try:
LOG(flags & 0xC5)

 
As I'm sure you know the reason this doesn't work is that the below sample of code is invalid.
 
cout << flags & 0xC5;
 
You need something like
 
cout << (flags & 0xC5);
 
Which would require a LOG invocation of
 
LOG((flags & 0xC5));
 
Not very nice I know Frown | :(
 
kozlowski wrote:
It is still far from reliability of the original TRACE
 
Though I'm not trying to define an exact replacement for TRACE. Rather what I want is something with most of the benefits of TRACE without associated drawbacks, i.e.
 
TRACE benifits
1/ zero release overhead
2/ minimum debug execution overhead (no runtime checks)
3/ no harder to use than the equivalent 'always log' statement (printf/cout)
 
TRACE drawbacks
4/ printf format string requires types to be explicity stated. This can be error prone, or impossible in templated code
5/ little control over final trace destination
6/ it's not possible to filter TRACE output (just compile it in or out)
 
Obviously there is a trade off between 2/ and 6. But I guess that's down to personal preference and usage of tracing/ logging within software development.
 
Though I do think this second proposed solution addresses 1/, 2/, 3/ and 4/. Plus rewriting the LOG macro would allow 5/ and 6/ to be addressed.
 
Cheers, Tom
GeneralRe: cool. and there is another way - An alternativememberkozlowski8 Jan '04 - 4:26 
I knew why the LOG(flags & 0xC5) is an error, as well as I was certain that you can't correct your macro definition to avoid error.
 
I would like to indicate that LOG is more sensitive for errors than TRACE. because comma is operator with lowest precedence and << is not.
 
TRACE benefits/drawbacks
Ad. 1 - It's requirement.
Ad. 2 - Doesn't matter
Ad. 3 - I would say it should be robust for typical errors.
Ad. 4 - I completely agree, but I need very rare to trace complex objects.
Ad. 5 - Yes, and it can't be changed.
Ad. 6 - I do not agree. It is possible to define your own several TRACE macros and switch only desired set, like
#define TRACECAT TRACE
#define TRACEMOUSE TRACE
I do even not use the original TRACE, always with the redefinition.
 
And I also agree that the preferences of programmer are very important.
 
Greetings,
Janusz
GeneralRe: cool. and there is another way - An alternativememberheyto8 Jan '04 - 7:04 

Ok, how about this? If your prepaired to abandon the stream notation (<<) in favour of a comma separated list and use the tricks as per this article. Plus the addition of discarding the whole function call via the __noop.
 
You get the following macro:
 
#define NDEBUG
  #define LOG __noop
#else
  #define LOG ::LogFunc
#endif
 
//
// Define similar for 1 - 10 argument functions
// i.e. T1, T2 def
//
template&lttypename T1, typename T2&gt static void LogFunc( T1 p1, T2 p2 )
{
	cout &lt&lt p1 &lt&lt p2;
}
 
 
then
 
LOG ( "hello world:", 20, somefunc() );
LOG ( flags & 0xC5 );
 
Both work as expected and reduce to nothing under Release builds. While supporting stream conversion and avoiding the need for format specifiers.
 
Cheers, Tom
GeneralRe: cool. and there is another way - An alternativememberkozlowski8 Jan '04 - 22:23 
OK, OK, OK,
I give up.
 
Solution fulfills all requirements specified by me.
 
But I still have doubts why MS did not defined TRACE in this way.
I suppose they had a reason. Maybe some different projects than console like DLLs.
 
Greetings,
Janusz

GeneralRe: cool. and there is another way - An alternativememberheyto8 Jan '04 - 23:22 

Playing devil's advocate Smile | :)
 
The TRACE definition that MS propose is compatible with the ANSI TRACE macro/function (printf style) and will work with pure 'C' code. So I guess it's preserving a accepted/ tried and tested interface. (I'll shut up now)
 
Cheers for the discussion,
 
Tom
GeneralRe: cool. and there is another way - An alternativememberkozlowski10 Jan '04 - 0:06 
Excellent point.
 
That's exactly the reallity of my work. C++ code of the Windows tools is mixed with C code from embedded systems.
 
Thanks for discussion. I hope it was helpful for the others.
Janusz

QuestionUse template defaults?memberSBarney31 Dec '03 - 11:10 
I believe some compilers support default "values" for template arguments. In that case, you could maybe write one actual function, something like this ...
template< typename T1, typename T2 = char*, typename T3 = char*, typename T4 = char* > 
  static void Trace( T1 par1, T2 par2="", T3 par3="", T4 par4="" )
{
stringstream ss;
   ss << par1 << par2 << par3 << par4;
   Trace( ss.str() );
}
... and it would automatically handle 1 through 4 arguments.
 
Copout: I don't have a compiler nor a reference book handy, so I'm not sure the above code is legal.
 
-- Scott
AnswerRe: Use template defaults?memberGuimaSun1 Jan '04 - 8:12 
Thanks for your comment Scott Smile | :)
I'm not so up to date about C++ standards, but I believe that default arguments on C++ templates is YET a specification question. Unfortunately the default argument doesn't work...you can try a simple construction like:
 
template <typename T1> static void f( T1 par1=1 ) // "" also results on error
{
cout << par1 << std::endl;
}
 
void main()
{
f();
}
 
...and we'll get an "error C2783: 'void __cdecl f(T1)' : could not deduce template argument for 'T1'"
 
Thanks again
 
GuimaSun
www.nexsun.com.br
GeneralRe: Use template defaults?sussAnonymous2 Jan '04 - 8:26 
I think it can be done as Scott suggested, but the on-the-wall static functions need to be moved to a class somewhere, perhaps as a baseclass for the #define'd MyTraceClass
 
template <typename T1=char*>
class DOBOB {
public:
   static void f( T1 par1="" ) { std::cout << par1 << std::endl ; }
} ;
 
then one can invoke DOBOB<>::f() ;
 
Jon
GeneralRe: Use template defaults?memberGuimaSun3 Jan '04 - 16:20 
This way we can't pass non-string data...
 
GuimaSun
www.nexsun.com.br
GeneralCool!memberJason Hattingh29 Dec '03 - 22:53 
I like your fresh approach to it - thank you, it will be very well used Wink | ;)
 
regards,
 
Jason Hattingh
http://www.greystonefx.com

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 29 Dec 2003
Article Copyright 2003 by GuimaSun
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid