Borislav Stanimirov
Borislav Stanimirov

Reputation: 1659

Is it possible to create a variadic macro which generates a forwarding function?

I'm pretty sure the answer to this question is a "no" but I would be very glad to be proven wrong

Say I have a variadic template function

template <typename... Args>
void log(Args&&... args) { ... }

...and I also have macros to generate concrete logging functions. Like:

#define LOG_2(fname, arg0_type, arg0_name, arg1_type, arg1_name) \
  void fname(arg0_type arg0_name, arg1_type arg1_name) { \
    log(std::forward<arg0_type>(arg0_name), std::forward<arg1_type>(arg1_name)); }

Macros named LOG_0 to LOG_10 with similar code also exist.

I use them to generate special logging functions which serve two purposes. First they give strong types to the arguments, and second they give them names, which with code-completion cam be used as hints. Example:

LOG_2(progress_log, const char*, msg, float, part)
...
progress_log("Loading: ", complete/total);

Then besides the strong typing of the args, I get the following nice code completion hint:

code completion hint

So, is it possible to drop the explicit arity in the macros and write something which has the exact same effect but uses variadic macros? ...and also isn't macro which uses a counter to call one of the underlying arity-macros by concatenating the appropriate number with LOG_.

I am aware I can count the arguments but most code-completion tools utterly fail to expand counting macros, especially if the possible argument count is big (like 10). Thus the code-completion effect gets lost. And if I would be willing to drop the code-completion hint effect, then I'd rather go with a something like LOG_FUN(progress_log, void(const char*, float)) which can also be used to generate compilation errors, but can have no argument names as hints

Upvotes: 2

Views: 952

Answers (2)

aschepler
aschepler

Reputation: 72431

The Boost.Preprocessor library is made for just this sort of case when you really want to do something fancy with preprocessor macros. It can get a bit ugly, though.

#include <boost/preprocessor/variadic/to_seq.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/control/if.hpp>
#include <boost/preprocessor/arithmetic.hpp>
#include <boost/preprocessor/punctuation.hpp>
#include <boost/preprocessor/logical.hpp>
#include <utility>

template <typename... Args>
void log(Args&&...) {}

#define DEF_LOG_COMMA_BEFORE_PAIR(i) \
  BOOST_PP_COMMA_IF(BOOST_PP_AND(i, BOOST_PP_NOT(BOOST_PP_MOD(i,2))))

#define DEF_LOG_EXPAND_PARAM(r, data, i, elem) \
  DEF_LOG_COMMA_BEFORE_PAIR(i) elem

#define DEF_LOG_EXPAND_CALL(r, data, i, elem) \
  DEF_LOG_COMMA_BEFORE_PAIR(i) \
  BOOST_PP_IF( BOOST_PP_MOD(i,2), \
    (elem), \
    std::forward<elem> )

#define DEF_LOG_FUNC(name, ...) \
  inline void name( \
    BOOST_PP_SEQ_FOR_EACH_I(DEF_LOG_EXPAND_PARAM, ~, \
      BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \
  ) { log( \
    BOOST_PP_SEQ_FOR_EACH_I(DEF_LOG_EXPAND_CALL, ~, \
      BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \
  ); }

DEF_LOG_FUNC(progress_log, const char*, msg, float, part)

int main() {
    progress_log("Progress", 0.25);
}

To pull this apart, first look at the DEF_LOG_COMMA_BEFORE_PAIR macro. It's just a helper that expands to a comma if the argument i is an even number other than zero, and expands to nothing otherwise. By putting it before the result of transforming one vararg parameter, we get a comma every other item. (After might seem more natural, but it would be a little more complicated to exclude a comma after the last term than this way of excluding the comma before the first item.) Note BOOST_PP_COMMA_IF is used to avoid expanding to a comma too early, which would confuse the other Boost macros.

Jumping down to the definition of the main macro DEF_LOG_FUNC, you'll see it uses BOOST_PP_SEQ_FOR_EACH_I twice. This is a tool which applies a supplied macro to each term from a passed sequence of arguments. The macro given must accept four arguments:

  • r: used when you need multiple levels of preprocessor loops

  • data: a sequence of tokens passed directly through BOOST_PP_SEQ_FOR_EACH_I to each invocation of the macro

  • i: the zero-based index of the sequence element

  • elem: the actual sequence element

Neither of our macros actually needs data, so we just pass ~ as a dummy token to both BOOST_PP_SEQ_FOR_EACH_I uses. The two helper macros we pass to BOOST_PP_SEQ_FOR_EACH_I are DEF_LOG_EXPAND_PARAM for building the function parameter list and DEF_LOG_EXPAND_CALL for building the argument list to the actual log function. DEF_LOG_EXPAND_PARAM doesn't need to do anything but add commas, using that DEF_LOG_COMMA_BEFORE_PAIR helper. DEF_LOG_EXPAND_CALL needs to do different things to even and odd arguments, so in addition to the DEF_LOG_COMMA_BEFORE_PAIR tool again, it uses BOOST_PP_IF to transform even (type) arguments to std::forward<elem> and odd (parameter) arguments to (elem).

So as desired, the preprocessor replaces

DEF_LOG_FUNC(progress_log, const char*, msg, float, part)

as

inline void progress_log(
    const char* msg , float part
) { log(
    std::forward<const char*> (msg) , std::forward<float> (part)
); }

Upvotes: 1

user7860670
user7860670

Reputation: 37600

You can use some tags instead. If you need to provide hint for special cases then you can write some explicit specializations:

enum Category{normal, progress};

template<Category category = normal, typename... Args>
void log(Args... args) { ... }

template<>
void log<progress, char const *, float>(char const *, float) { ... }

// typing this will show generic and specialized variant hints in VS2017
log<progress>("Loading: ", complete/total);

Upvotes: 1

Related Questions