Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variadic Recursive Template

I have a particular format that requires space delimited tokens with a final null terminator (the null is part of the output). I created a function to send a series of space delimited tokens to an output stream:

// C++ variadic template function to output tokens to a stream delimited by spaces
template <typename T>
void join(std::ostream& os, T const& arg)
{
  // This is the last argument, so insert a null in the stream to delimit
  os << arg << '\000';
}

// Join one, add a space, try again.
template <typename T, typename... Args>
void join(std::ostream& os, T const& arg, Args... args)  // recursive variadic function
{
  os << arg << " ";
  join(os, args...);
}

This works fine for things like join(os, 1, foo, "THING", M_PI);. However, I also have some tokens that need to be not space-delimited, like join(os, "KEY=", val);.

I tried to play with the arguments, thinking maybe I could stuff a nullptr in the argument list and use that to overload the method, skipping the space, but I cannot for the life of me figure out how to do that kind of overload.

Most of the questions on variadic templates I have seen are fairly old, before standards compliance was more ubiquitous. If this has been answered elsewhere, please point me to it. I'm using GCC 7.3.

Or, just tell me I am overly complicating what should be very simple. Maybe the variadic template is not the right hammer for this bolt?

like image 282
jwm Avatar asked Jan 29 '23 07:01

jwm


2 Answers

Introduce a helper

template <typename A, typename B>
struct pack_t {
  const A& a;
  const B& b;
};

template <typename A, typename B>
std::ostream& operator<<(std::ostream& os, pack_t<A, B> pac) {
  return os << pac.a << pac.b; 
}

template <typename A, typename B>
auto pack(const A& a, const B& b) noexcept {
  return pack_t<A, B>{a, b}; 
}

Use it like

join(std::cout, "Hello", "World", pack("pi=", 3.14));

Complete code (live)

#include <iostream>

template <typename A, typename B>
struct pack_t {
  const A& a;
  const B& b;
};

template <typename A, typename B>
std::ostream& operator<<(std::ostream& os, pack_t<A, B> pac) {
  return os << pac.a << pac.b; 
}

template <typename A, typename B>
auto pack(const A& a, const B& b) noexcept {
  return pack_t<A, B>{a, b}; 
}

template <typename T>
void join(std::ostream& os, const T& arg) {
  os << arg << '\0';
}

template <typename T, typename... Args>
void join(std::ostream& os, const T& arg, const Args&... args) {
  os << arg << ' ';
  join(os, args...);
}

int main() {
  join(std::cout, "Hello", "World", pack("pi=", 3.14));
}

Note that you can extend the helper to support more-than-two arguments by basing pack_t on std::tuple via aggregation or inheritance. For example,

namespace impl {

template <typename T, std::size_t... Idx>
struct pack_t {
  T v;
};

template <std::size_t... Idx, typename... Ts>
auto pack(std::index_sequence<Idx...>, Ts&&... vs) noexcept {
  auto v = std::forward_as_tuple(std::forward<Ts>(vs)...);
  return pack_t<decltype(v), Idx...>{std::move(v)};
}

template <typename T, std::size_t... Idx>
std::ostream& operator<<(std::ostream& os, pack_t<T, Idx...> args) {
  return ((os << std::get<Idx>(std::move(args.v))), ...);
}

}

template <typename... Ts>
auto pack(Ts&&... vs) noexcept {
  return impl::pack(
    std::index_sequence_for<Ts...>{}, std::forward<Ts>(vs)...);
}

Now, you can pack a varying number of arguments (live).

like image 180
Lingxi Avatar answered Feb 05 '23 06:02

Lingxi


I suppose you can select a special type to skip the preceding space; by example you can define a no_space_tag class or struct

struct no_space_tag {};

and add a join() version that intercept a no_space_tag object in second position (third, counting os) and doesn't add the space after the first (second) element; I mean

template <typename T, typename ... Args>
void join (std::ostream & os, T const & arg, no_space_tag const &,
           Args ... args)
 {
   os << arg;
   join(os, args...);
 }

so you can add a no_space_tag object in this way

join(std::cout, "KEY=", no_space_tag{}, val);

or also in this way

no_space_tag  nst;

join(std::cout, "KEY=", nst, val, ',', "KEY=", nst, val2);

to avoid to add the preceding space.

The following is a full working example

#include <iostream>

struct no_space_tag {};

template <typename T>
void join (std::ostream & os, T const & arg)
 { os << arg << '\000'; }

template <typename T, typename ... Args>
void join (std::ostream & os, T const & arg, Args ... args)
 {
   os << arg << " ";
   join(os, args...);
 }

template <typename T, typename ... Args>
void join (std::ostream & os, T const & arg, no_space_tag const &,
           Args ... args)
 {
   os << arg;
   join(os, args...);
 }

int main()
 {
   char foo {'a'};
   long val { 42L };

   join(std::cout, 1, foo, "THING", "M_PI");
   join(std::cout, "KEY=", no_space_tag{}, val);
 }
like image 43
max66 Avatar answered Feb 05 '23 06:02

max66