2

I have the following structs:

struct s0 {
    char name[64];
    s0* parent;
    int stackLevel;

    s0(s0* parent_, char* nameMask_, ...){
        va_list args;
        va_start(args, nameMask_);
        vsprintf_s(name, 64, nameMask_, args);
        va_end(args);
        parent=parent_;
    }

    ~s0(){}
};

struct s1 : s0 {
   int p1;

   s1(s0* parent_, int p1_, char* nameMask_, ...) : s0(parent_, nameMask_) {
        p1=p1_;
   }
}

I am looking for a way to have s1 constructor forward its variadic arguments to s0 constructor. I started looking into parameter packs and variadic templates, but I just can't see how that would work in this context. Is what I am looking for feasible at all?

gcaglion
  • 131
  • 1
  • 12
  • 1
    The classic "C" solution to this is to have a function (or, in your case, constructor in s0) that takes a `va_list` argument, just like vsprintf does. The s1 constructor can then call this. You will need a common (private) 'helper' method in s0 that both s0 constructors can call to common up the initialisation code that takes a `va_list` and does the real work. Or maybe s0 doesn't need a variadic constructor at all, depends on your use case. The tag `variadic-templates` is out of place here, BTW. You aren't using templates. – Paul Sanders May 26 '18 at 09:34
  • 1
    You are using variadic functions, not variadic templates. If you are asking how to pass the variadic arguments to another function, it is answered [here](https://stackoverflow.com/q/3530771/218774). – J. Calleja May 26 '18 at 10:31
  • I see how the va_list argument can be handled by the s0 'helper' constructor, but this doesn't help me , as the s1 constructor that gets called by other classes in my application is the one with the "..." argument. How can I process that into a va_list argument before passing it to s0 constructor, as it is on the very same line? – gcaglion May 26 '18 at 10:45

2 Answers2

0

Some notes before...

  1. Variable arguments add something essential to C. (Without them, the printf() family couldn't hardly be implemented in pure C.) It is based on macros which close the gap between unique code and varying implementations which are very platform and hardware dependent. In C++, the use of such macros should be prevented in general as much as possible. templates provide a type-safe alternative.

  2. Considering that the C++ replacement for the printf() family are the stream classes with a variety of overloaded shift operators, IMHO, in certain situations the principle with a format string and a varying number of arguments is much more appropriate (e.g. for output of notifications or log messages at runtime where, additionally localization shall be applied which even may change the output order of formatted arguments). Stream classes with shift operators cannot be used well for this. Some years ago, when we used gtkmm I found Glib::ustring::compose() very appropriate for this job. Though, it's (as well as printf()) not "compile time safe" it's at least type safe and runtime checks can be easily applied. And: AFAIK, it's free of macros – implemented in pure C++ code only.

However, the above disclaimer in mind, I tried to understand/solve the issue of OP concerning an implementation with variable arguments. When writing the sample code, I just realized why it becomes a problem to call a constructor accepting va_list out of another constructor. Following the scheme which is nicely presented in the answer to SO: Passing variable arguments to another function that accepts a variable argument list the following code forms:

struct s0 {
  s0(const char *fmt, va_list args);
};

struct s1: s0 {
  s1(const char *fmt, ...):
    va_list args;
    va_start(args, fmt);
    s0(fmt, args)
    va_end(args);
  { }
};

Ouch, wrong! I really would wonder if there is any platform/hardware combination where this will compile successfully. So, the call of base class constructor has to be moved into the body:

struct s1: s0 {
  s1(const char *fmt, ...)
  {
    va_list args;
    va_start(args, fmt);
    s0(fmt, args);
    va_end(args);
  }
};

Ouch, wrong again! Beyond the fact that I even don't know wether a constructor might be called in the body of a function, I cannot prevent the implicit call of the default constructor in this case which is not available.

So, I ended up in the certainty that constructor with va_list is not the solution in this case. I don't see any other way as separating the function which gets/processes the va_list:

#include <iostream>
#include <cstdio>
#include <cstdarg>

struct s0 {
    char name[64];
    s0 *parent;
    int stackLevel;

    s0(s0 *parent, const char *nameMask, ...):
      parent(parent)
    {
      va_list args;
      va_start(args, nameMask);
      vsnprintf(name, sizeof name, nameMask, args);
      va_end(args);
    }

    ~s0() = default;

  protected:
    // constructor for derived classes
    s0(s0 *parent): name(""), parent(parent) { }

    // construction of name
    void initName(const char *nameMask, va_list args)
    {
      vsnprintf(name, sizeof name, nameMask, args);
    }
};

struct s1: s0 {
    int p1;

    s1(s0 *parent, int p1, const char *nameMask, ...):
      s0(parent), p1(p1)
    {
      va_list args;
      va_start(args, nameMask);
      initName(nameMask, args);
      va_end(args);
    }

    ~s1() = default;
};

int main(int argc, char *argv[])
{
  s0 s0(nullptr, "s0[%d]", 0);
  std::cout << "s0 '" << s0.name << "'\n";
  s1 s1(&s0, 1, "s1[%d]", 1);
  std::cout << "s1 '" << s1.name << "'\n";
  return 0;
}

Output:

s0 's0[0]'
s1 's1[1]'

Live Demo on coliru

Note:

While implementing the sample I realized that vsprintf_s() is part of the C11 standard but not C++11. Hence, I replaced it with std::vsnprintf() which works sufficiently similar.

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
0

As you suggest, it is possible to use variadic templates if you can use C++11. The new s0 constructor would be something like:

template<typename ... Args>
s0(s0* parent_, const char* nameMask_, Args... args) : parent(parent_) {
  sprintf(name, nameMask_, args...);
}

This construction can also be used for s1 in order to directly call the constructor of s0.

However, I think there are two issues to take into account:

  • Now that the constructor is templatized, the compiler creates a new constructor for each parameter list. The compilation time Will increase.
  • Now, the type of the parameters is known inside the constructor. However, the code does not use this data because it is calling sprintf that uses variadic arguments. It may be interesting to use a different formatting library.

The whole code would be like:

#include <iostream>
#include <cstdio>

struct s0 {
  char name[64];
  s0* parent; 
  int stackLevel;

  template<typename ... Args>
  s0(s0* parent_, const char* nameMask_, Args... args)
    : parent(parent_) {
    sprintf(name, nameMask_, args...);
  }
};

struct s1 : s0 {
  int p1;

  template<typename ... Args>
  s1(s0* parent_, int p1_, const char* nameMask_, Args... args)
    : s0(parent_, nameMask_, args...), p1(p1_) {
  }
};
J. Calleja
  • 4,855
  • 2
  • 33
  • 54