Home > Net >  C : Member function call forwarding using variadic data structure
C : Member function call forwarding using variadic data structure

Time:12-01

I am trying to create a class (similar to std::tuple) which can hold a variable number of references to objects of different type and which can forward a call to a specific member function to all of its constituents. Here is an example:

// g   Test7.C -std=c  17 -o Test7

#include <iostream>

struct P1 { void operator()(void) const { std::cout<<"P1 "; } };
struct P2 { void operator()(void) const { std::cout<<"P2 "; } };
struct P3 { void operator()(void) const { std::cout<<"P3 "; } };

template <class... EmitterList> struct Emitters 
{
 inline void operator()(void) const { std::cout<<std::endl; }
};

template <class Emitter, class... EmitterList>
class Emitters<Emitter,  EmitterList...> : public Emitters<EmitterList...>
{
public:
 using Base = Emitters<EmitterList...>;

 Emitters(const Emitter & e, const EmitterList & ... eList)
  : emitter(e), Base(eList...) {}

 inline void operator()(void) const { emitter(); this->Base::operator()(); }
private:
 const Emitter & emitter;
};

int main(void)
{
 P1 p1;
 P2 p2;
 P3 p3;
 Emitters e0;           e0(); // works
 //Emitters e1{p1};       e1(); // does not work
 //Emitters e2{p1,p2};    e2(); // does not work
 //Emitters e3{p1,p2,p3}; e3(); // does not work
 return 0;
}

The expectation is that e1() would output "P1 \n" (calls p1()), e2() would output "P1 P2 \n" (calls p1(); p2()), and e3() would output "P1 P2 P3 \n" (calls p1(); p2(); p3();). But something is not right in the code: it compiles for e0 but not for the other cases. Could you please help me to understand what I am doing not right here and how to fix it?

Thank you very much for your help!

CodePudding user response:

It looks like you're trying to do partial specialization, and you're close. Rather than this

template <class... EmitterList> struct Emitters 
{
 inline void operator()(void) const { std::cout<<std::endl; }
};

Consider

template <class... Ts>
struct Emitters;

template <>
struct Emitters<> {
  void operator()(void) const { std::cout<<std::endl; }
};

template <class Emitter, class... EmitterList>
class Emitters<Emitter,  EmitterList...> : public Emitters<EmitterList...>
{
  // Unmodified from your example ...
}

The first declaration here says "there's a thing called Emitters, but I'm not telling you what it is yet". It's an incomplete type definition. Then we define two cases: one with Emitters<> (the empty, base case), and one (the recursive case) for when we have at least one argument.

These classes will compile now, but they're inconvenient to use. Due to complexities in the language, constructor calls aren't as good as inferring template arguments as regular functions. So a line like this

Emitters e1{p1};

isn't good enough. We could be explicit

Emitters<P1> e1{p1};

but that's just a pointless level of verbosity. Instead, we can look to std::tuple, which has the same problem. The standard library defines a function std::make_tuple, which does nothing but call the tuple constructor. But since it's an ordinary function, we get template argument deduction.

template <typename... Ts>
Emitters<Ts...> make_emitters(const Ts&... args) {
  return Emitters<Ts...>(args...);
}

Now we can make instances without specifying template arguments

Emitters e0 = make_emitters();         e0();
Emitters e1 = make_emitters(p1);       e1();
Emitters e2 = make_emitters(p1,p2);    e2();
Emitters e3 = make_emitters(p1,p2,p3); e3();

Now your example should work as intended.

One final note about your constructor. You should always list the base class constructor first, before any fields. Compiling with -Wall will warn you about this. The compiler is internally reordering them for you, so it's best practice to put them in the right order to begin with. Rather than

Emitters(const Emitter & e, const EmitterList & ... eList)
 : emitter(e), Base(eList...) {}

consider

Emitters(const Emitter & e, const EmitterList & ... eList)
 : Base(eList...), emitter(e) {} // Note: Base and emitter are switched

Complete example:

#include <iostream>

struct P1 {
  void operator()(void) const {
    std::cout << "P1 ";
  }
};
struct P2 {
  void operator()(void) const {
    std::cout << "P2 ";
  }
};
struct P3 {
  void operator()(void) const {
    std::cout << "P3 ";
  }
};

template <class... Ts>
struct Emitters;

template <>
struct Emitters<> {
  void operator()(void) const {
    std::cout << std::endl;
  }
};

template <class T, class... Ts>
struct Emitters<T,  Ts...> : public Emitters<Ts...> {
public:
  using Base = Emitters<Ts...>;

  Emitters(const T& e, const Ts&... eList)
    : Base(eList...), emitter(e) {}

  void operator()(void) const {
    emitter();
    this->Base::operator()();
  }

private:
  const T& emitter;
};

template <typename... Ts>
Emitters<Ts...> make_emitters(const Ts&... args) {
  return Emitters<Ts...>(args...);
}

int main(void) {
  P1 p1;
  P2 p2;
  P3 p3;
  Emitters e0 = make_emitters();         e0();
  Emitters e1 = make_emitters(p1);       e1();
  Emitters e2 = make_emitters(p1,p2);    e2();
  Emitters e3 = make_emitters(p1,p2,p3); e3();
  return 0;
}

CodePudding user response:

The other answer is great, but you don't actually need any of these changes. The partial specialisation cleanup is nice and elegant, I would suggest using it, but your original code works too. And the make_... functions are clunky and mostly unneeded in modern C . What you need is a deduction guide:

template <typename ... P>
Emitters(const P& ... ) -> Emitters<P...>;

That's it.

  • Related