-1

I came across this function below written in C++. A call to the function trace() with any number of arguments prints the values of each of these arguments alongside the name of the argument in the format

name1 : value1 | name2 : value2 and so on.

I wanted to learn how this code is working and what some of the syntax like the double ampersand &&, __VA_ARGS__ meant. Thanks!

#define tr(...) trace(#__VA_ARGS__, __VA_ARGS__)

template <typename Arg1>
void trace(const char* name, Arg1&& arg1){
    cout << name << " : " << arg1 << endl;
}

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

    trace(comma+1, args...);
}
Banach Tarski
  • 1,821
  • 17
  • 36
  • 3
    `&&` is a [rvalue reference](http://stackoverflow.com/questions/5481539/what-does-t-double-ampersand-mean-in-c11) and `#` is the [stringize operator](http://stackoverflow.com/questions/16989730/stringification-how-does-it-work) of the c++ preprocessor. – πάντα ῥεῖ Sep 11 '16 at 08:27
  • @πάνταῥεῖ in this case it is a forwarding reference. It may resolve to an lvalue reference depending on the arguments provided. – M.M Sep 11 '16 at 08:31
  • @M.M Does that actually matter in how that stuff expands? Write an answer if you think its worth it for a lazy researched question. – πάντα ῥεῖ Sep 11 '16 at 08:33
  • @πάνταῥεῖ yes: a rvalue reference cannot bind to an lvalue argument, but a forwarding reference can. – M.M Sep 11 '16 at 09:16
  • @M.M Sure, I have to admit perfect forwarding is a significant point here. My expectations for good questions here are probably too high now, I'll need to be rooted again. – πάντα ῥεῖ Sep 11 '16 at 09:18
  • (to OP) [This answer](http://stackoverflow.com/a/3582313/1505939) explains forwarding references and reference collapsing in detail -- the key point to understand is that when matching `T&&`, and `T` is a template parameter, then `T` is deduced to a reference type (e.g. `int&`) for an lvalue argument. This is the only time in all of C++ where template parameter deduction makes the deduced type be a reference type. – M.M Sep 11 '16 at 09:27

2 Answers2

5
#define tr(...) trace(#__VA_ARGS__, __VA_ARGS__)

is a function macro that accepts a variable number of arguments.
It forwards a stringified version as the first argument.
There is no way to derive a string representation without using the preprocessor.

Example evaluation:

int main(){
    int i;
    float f;
    std::string s;
    tr(i,f,s);
}

resolves to:

int main(){
    int i;
    float f;
    std::string s;
    trace("i,f,s", i,f,s);
}

The two variadic template functions unroll each argument and calls itself recursively.

The base case function that ends the recursion is:

template <typename Arg1>
void trace(const char* name, Arg1&& arg1){
    cout << name << " : " << arg1 << endl;
}

This could be done more cleanly with a fold expression.

&& is used to allow perfect forwarding.

Trevor Hickey
  • 36,288
  • 32
  • 162
  • 271
0

This function depends on several mechanics:

  • Variadic templates: to support arbitrary number of parameters, the function is defined as a variadic template. This feature has recursive syntax, like the one you could meet in functional languages: template <typename Arg1, typename... Args> where Arg1 is the first argument, and Args are all the rest. Hence the recursive definition of the function itself - to unpack the parameters, it calls itself for the tail part of argument list. Notice the comma-names trick to calculate the length of a parameter name.
  • Preprocessor stringize operator: __VA_ARGS__ is of course simply the macro for the list of arguments of tr(...) (comma separated). Adding the # before it is what creates the string out of it, which is passed as the first argument in the trace(const char* names, ...).
  • Reference collapsing (Arg&& in the templates): this is actually the most complicated thing here, which is sad, because it does pretty much nothing. You can literally remove the &&s and the code would still work as expected. What && does is it adjusts the reference type of expected arguments to match what is actually passed, e.g. if you pass an argument by reference, it will not copy it, and if you pass a temporary object, it will use the move semantics. The implementation technique of this feature is a bit painful to talk about, so here is another SO question for further reading
Community
  • 1
  • 1
Ap31
  • 3,244
  • 1
  • 18
  • 25
  • The code would be different if `&&` is removed; passing arguments by value will invoke a copy operation which may have side-effects. May even fail to compile for non-copyable objects. – M.M Sep 11 '16 at 09:11
  • @M.M oh yeah, good point about the `std::forward`. As for the copy side-effects - of course I meant that overall behavior would remain the same, presuming that all the constructors comply to their respective contracts, and the underlying semantics would most definitely change – Ap31 Sep 11 '16 at 09:22
  • 1
    (sorry, rewriting my previous comment as it had a mistake) - there is reference collapsing but I think the original code is bugged because the recursive call should use `std::forward(args)...` – M.M Sep 11 '16 at 09:25