0

In the following code, I would like to find a more generic way of calling GenericPublish__Advertise(), which takes a variadic template list. What could I do to improve it?

I would like to map topics to a publisher of a specific type:

  • topic[0] -> publisher[0]
  • topic[1] -> publisher[1]
  • so forth

While the code works ok, I need to manually write the templated versions of GenericPublish__Advertise() and manually map topics[i] to publishers. I would like to somehow generalize the implementation of GenericPublish__Advertise().

Many thanks in advance.

Code:

    #include <iostream>
    #include <memory>
    #include <typeinfo>
    #include <vector>

    class AdvertiseOptionsBase {
    public:
      virtual const std::type_info &GetType() = 0;
    };

    template <typename TSend> 
    class AdvertiseOptions : public AdvertiseOptionsBase {
    public:
      AdvertiseOptions(TSend opt) : opt_(opt) {}
      const std::type_info &GetType() { return typeid(opt_); }

    private:
      TSend opt_;
    };

    class Publisher {
    public:
      Publisher(const std::string &topic) : topic_(topic) {}
      const std::string &GetTopic() const { return topic_; }
      template <typename TSend>
      void SetOptions(const AdvertiseOptions<TSend> &opt) {
        options_ = std::make_unique<AdvertiseOptions<TSend>>(opt);
      }
      const std::unique_ptr<AdvertiseOptionsBase> &GetOptions() const {
        return options_;
      }

    private:
      std::string topic_;
      std::unique_ptr<AdvertiseOptionsBase> options_;
    };

    class Node {
    public:
      template <typename TSend>
      Publisher advertise(std::string topic) {
        Publisher publisher(topic);
        TSend option;
        AdvertiseOptions<TSend> options(option);
        publisher.SetOptions<TSend>(options);
        return publisher;
      }
    };

    template <typename TSend1, typename TSend2>
    void GenericPublish__Advertise(Node &node, std::vector<Publisher> &publishers,
                                   const std::vector<std::string> &topics) {
      publishers.push_back(node.advertise<TSend1>(topics.at(0)));
      publishers.push_back(node.advertise<TSend2>(topics.at(1)));
    }
    template <typename TSend1, typename TSend2, typename TSend3>
    void GenericPublish__Advertise(Node &node, std::vector<Publisher> &publishers,
                                   const std::vector<std::string> &topics) {
      publishers.push_back(node.advertise<TSend1>(topics.at(0)));
      publishers.push_back(node.advertise<TSend2>(topics.at(1)));
      publishers.push_back(node.advertise<TSend3>(topics.at(2)));
    }

    template <typename... TSend>
    class GenericPublish {
    public:
      GenericPublish(const std::vector<std::string> &topics) {
        GenericPublish__Advertise<TSend...>(node_, publishers_, topics);
      }
      void PrintInfo() {
        for (const auto &publisher : publishers_) {
          std::cout << publisher.GetTopic() << " -----> "
                         << (publisher.GetOptions()->GetType()).name() << std::endl;
        }
      }

    protected:
      Node node_;
      std::vector<Publisher> publishers_;

    private:
    };

    int main() {
      std::vector<std::string> topics({"topic_int", "topic_double"});
      GenericPublish<int, double> o(topics);
      o.PrintInfo();
      return 0;
    }
Mihai Galos
  • 1,707
  • 1
  • 19
  • 38

2 Answers2

3

The typical approach here is to use the index sequence trick. You take a parameter pack of types, construct an index sequence of the same size, and then iterate over both:

template <typename... TSends> // <== pack of types
void GenericPublishAdvertise(Node &node, std::vector<Publisher> &publishers,
                             const std::vector<std::string> &topics)
{
  GenericPublishAdvertiseImpl<TSends...>(node, publishers, topics,
      std::index_sequence_for<TSends...>()); // <== index sequence creation
}

with the separate implementation doing:

template <typename... TSends, size_t... Is>
void GenericPublishAdvertiseImpl(Node &node, std::vector<Publisher> &publishers,
    const std::vector<std::string> &topics, std::index_sequence<Is...> )
{
    // since this is C++14
    using swallow = int[];
    (void)swallow{0,
        (void(publishers.push_back(node.advertise<TSends>(topics.at(Is)))), 0)...
        };

    // in C++17, that's just
    (publishers.push_back(node.advertise<TSends>(topics.at(Is))), ...);
}

See this answer for explanation of that pattern.


Note that using GenericPublish__Advertise is UB: names with double underscores are reserved.

Barry
  • 286,269
  • 29
  • 621
  • 977
2
template <class ... TSends, std::size_t ... Is>
GenericPublish__Advertise_impl(Node &node, std::vector<Publisher> &publishers,
                               const std::vector<std::string>& topics, std::index_sequence<Is...>)
{
    (void)int x[] = {(publishers.push_back(node.advertise<TSends>(topics.at(Is))), 0)...};
} 


template <class ... TSends>
GenericPublish__Advertise((Node &node, std::vector<Publisher> &publishers,
                               const std::vector<std::string>& topics)
{
    GenericPublish__Advertise_impl(node, publishers, topics, std::index_sequence_for<TSends...>{});
}

There's two tricks here which are pretty standard when you want to do some kind of indexing over a variadic pack. The first thing is that you delegate to an implementation function, passing it all the arguments, plus this std::index_sequence type. This lets the implementation function deduce a pack of integers which will be numbered 0 to N-1 a pack of size N. Second, you initialize an unused dummy array, and use the comma operator to discard the return (or lack thereof) of whatever you are doing, and simply return 0.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72