trace(...) __f(#__VA_ARGS__, __VA_ARGS__)

I have seen these lines are added by many programmers in their code. Anyone please explain use of these things?

#define TRACE

#ifdef TRACE
#define trace(...) __f(#__VA_ARGS__, __VA_ARGS__)
template <typename Arg1>
void __f(const char* name, Arg1&& arg1){
  cerr << name << " : " << arg1 << std::endl;
}
template <typename Arg1, typename... Args>
void __f(const char* names, Arg1&& arg1, Args&&... args){
  const char* comma = strchr(names + 1, ',');cerr.write(names, comma - names) << " : " << arg1<<" | ";__f(comma+1, args...);
}
#else
#define trace(...)
#endif

Thanks in advanceā€¦

5 Likes

It is matter of taste to debug your crushed program with debugger or with debug output via trace calls.

Typical second approach steps:

  1. locate program crush place with some kind of binary search by inserting trace(1), trace(2), ā€¦ in some places of your program. Some trace calls will print to stderr, but some will not.
  2. dump some variables with trace(i, j, k) calls for understand crush reason
  3. after fixing bug remove #define TRACE line from program and submit it to judge

Example of stderr output of trace(i,j,k) call.

i : 1 |  j : 2 |  k : 3

If TRACE is defined (debug mode) then macro trace(i,j,k) transforms to call _f(ā€œi,j,kā€, i, j, k) which execute statement cerr "i : " << i << " | " and calls _f(ā€œj,kā€, j, k).
Otherwise (in submit mode) trace(i,j,k) is ignored.

Example.cpp

#include <cstring>
#include <iostream>
using namespace std;
int factorial (int x) {
  if (!x) return 1;
  return x * factorial (x - 1);
}
#define TRACE
#ifdef TRACE
#define trace(...) __f(#__VA_ARGS__, __VA_ARGS__)
template <typename Arg1>
void __f(const char* name, Arg1&& arg1){
  cerr << name << " : " << arg1 << std::endl;
}
template <typename Arg1, typename... Args>
void __f(const char* names, Arg1&& arg1, Args&&... args){
  const char* comma = strchr(names + 1, ',');cerr.write(names, comma - names) << " : " << arg1<<" | ";__f(comma+1, args...);
}
#else
#define trace(...)
#endif
int main (){
  int i = 1, j = 2, k = 3;
  trace (i,j,k);
  trace (1 << 30);
  trace(factorial(1), factorial(2), factorial(3), factorial(4));
}

Output

i : 1 | j : 2 | k : 3
1 << 30 : 1073741824
factorial(1) : 1 |  factorial(2) : 2 |  factorial(3) : 6 |  factorial(4) : 24
7 Likes

Can someone explain the concept behind these templates, variadic function etc going there.

Iā€™ll try to break down and explain the following lines:

#define trace(...) __f(#__VA_ARGS__, __VA_ARGS__)
template <typename Arg1>
void __f(const char *name, Arg1 &&arg1)
{
    cerr << name << " : " << arg1 << endl;
}

template <typename Arg1, typename... Args>
void __f(const char *names, Arg1 &&arg1, Args&&... args)
{
    const char *comma = strchr(names + 1, ',');
    cerr.write(names, comma - names) << " : " << arg1 << " | ";
    __f(comma + 1, args...);
}

The line:

#define trace(...) __f(#__VA_ARGS__, __VA_ARGS__)
  • defines a Variadic macro trace(...) which takes a variable number of arguments (variadic). We know this because it uses the ellipsis (...) operator.
  • The sequence of tokens inside the parenthesis replaces the special identifier __VA_ARGS__.
  • # is Stringizing operator. The stringizing operator converts macro parameters to string literals.

See this link: https://ideone.com/xozuuK.

Side note about usage of double underscores (or, dunders as Python programmers like to call it :D): https://stackoverflow.com/q/224397/9332801.

We all have been using variadic function in C/C++ without even realizing it. printf is one such example :p. It allows you to pass variable number of arguments.

Now, we know that all the trace() calls will be replaced by __f() calls. For example:

trace(a)       -> __f("a", a)
trace(a, b)    -> __f("a, b", a, b)
trace(a, b, c) -> __f("a, b, c", a, b, c) and so on...

The following statement:

template <typename Arg1>					// (a)
template <typename Arg1, typename... Args>	// (b)

basically allows us to use trace() with any data type. It allows us to code generically in C++. For example, we are able to create both std::vector <int> or std::vector <float> because std::vector is a templated class.

We can also see that __f() is overloaded because it has two distinct definitions. One takes only two arguments (letā€™s refer to it as f1 for brevity) and another takes variable number of arguments (similarly f2).

The f2 is a recursive function and f1 is its base case. Whenever we call __f() with more than 2 arguments, f2 will pick it up, do something (print the name and value of first argument in this case) and pass the rest of the arguments in f2 again (by a recursive call). In the end when it calls __f() with exactly 2 arguments , f1 will be executed (as a base case).

const char* names takes the string passed by the Stringizing operator (# in #__VA_ARGS__) in both of these functions.

int&& x denotes an rvalue reference. To be honest, I donā€™t fully grasp the concept of rvalues and lvalues in C/C++. I just have a basic idea. For now, we can probably think of it as being a reference to the actual value of the variable. For example, take a look at this code: https://ideone.com/r5G5iy.

So, considering trace(a, b, c, d) as an example, the macro will be translated as __f("a, b, c, d", a, b, c, d). This has more than 2 arguments, so it will go to the second definition of __f() where names will point to "a, b, c, d" and arg1 will take care of the second argument a and the rest b, c, d will be handled by args.

Now, strchr() will find the first occurrence of , in names (this will be pointed to by comma variable) and this will be printed. In the next call names will point to comma + 1 as we are done with the first argument and donā€™t need it any more. This process will be repeated until the last argument passed to trace() is processed.

Note: I am not sure why names+1 is passed to strchr(). Itā€™ll be great if someone can explain it.

cerr will just output to error stream. Unless otherwise specified, cerr (error stream) and cout (output stream) are tied to the same output screen (terminal).

Thereā€™s a lot to be learned in these few lines of code :D. Itā€™s possible I missed something. If someone can add more information, please do.

I first encountered this snippet in the code of darkshadows.

4 Likes

I have a doubt, As u told ā€œlocate program crush place with some kind of binary search by inserting trace(1), trace(2), ā€¦ in some places of your program. Some trace calls will print to stderr, but some will notā€ similarly we can also find error by placing cout in some places of program,then what is the difference between both?

trace(1) outputs ā€˜1; 1ā€™. If you see ā€˜1; 1ā€™ in stderr line with trace(1) is reachable. It is some kind of fast typing for { cerr << ā€œbefore segment tree creationā€ << endl; create_segment_tree (); cerr << ā€œafter segment tree creationā€ << endl }. From stderr you can determine was create_segment_tree function crushed or not.
I know with gdb finding crush line is much easier task, but sometimes gdb isnā€™t available on local computer and question was how people use such trace function.

1 Like

@avm_ can you explain the internal working of whole trace function.

thnks @avm_ :slight_smile:

I suppose that it is safe to pass names+1 to strchr() since names[0] isnā€™t equal to comma (because single name contains at least one character).

@avm_ When I run the code with both names and names+1, I get the same result. If both give the same result in all the cases, then itā€™s mostly a source of confusion according to me. :expressionless: