Wrap cstdio print function with C++ variadic template

216 Views Asked by At

I'm writing a lightweight parsing library for embedded systems and try to avoid iostream. What I want to do is write variables to a buffer like vsnprintf() but I don't want to specify the format string, much rather the format string should be infered from the arguments passed to my variadic template wrapper. Here's an example:

#include <cstddef>
#include <string>
#include <cstdio>


template <typename... Ts>
void cpp_vsnprintf(char* s, size_t n, Ts&&... arg)
{
    std::vsnprintf(s, n, /*how to deduce format string?*/, arg...);
}

int main()
{
    char buf[100];

    cpp_vsnprintf(buf, 100, "hello", 3, '2', 2.4);
    printf(buf);
}

I'm looking for a performant solution, maybe the format string could be composed at compile time? Or is there maybe an stl function that does exactly what I'm asking for?

2

There are 2 best solutions below

0
On BEST ANSWER

I figured it out with some inspiration from this thread

#include <cstdio>

template<class T> struct format;
template<class T> struct format<T*>       { static constexpr char const * spec = "%p";  };
template<> struct format<int>             { static constexpr char const * spec = "%d";  };
template<> struct format<double>          { static constexpr char const * spec = "%.2f";};
template<> struct format<const char*>     { static constexpr char const * spec = "%s";  };
template<> struct format<char>            { static constexpr char const * spec = "%c";  };
template<> struct format<unsigned long>   { static constexpr char const * spec = "%lu"; };


template <typename... Ts>
class cxpr_string
{
public:
    constexpr cxpr_string() : buf_{}, size_{0}  {
        size_t i=0;
        ( [&]() {
            const size_t max = size(format<Ts>::spec);
            for (int i=0; i < max; ++i) {
                buf_[size_++] = format<Ts>::spec[i];
            }
        }(), ...);
        buf_[size_++] = 0;
    }

    static constexpr size_t size(const char* s)
    {
        size_t i=0;
        for (; *s != 0; ++s) ++i;
        return i;
    }

    template <typename... Is>
    static constexpr size_t calc_size() {
        return (0 + ... + size(format<Is>::spec));
    }

    constexpr const char* get() const {
        return buf_;
    }

    static constexpr cxpr_string<Ts...> ref{};
    static constexpr const char* value = ref.get();
private:
    char buf_[calc_size<Ts...>()+1] = { 0 };
    size_t size_;
};

template <typename... Ts>
auto cpp_vsnprintf(char* s, size_t n, Ts... arg)
{
    return snprintf(s, n, cxpr_string<Ts...>::value, arg...);
}


int main()
{
    char buf[100];
    cpp_vsnprintf(buf, 100, "my R", 2, 'D', 2, '=', 3.5);
    printf(buf);
}

Demo

Output:

my R2D2=3.50

You can see that format strings are neatly packed into the binary:

        .string "%s%d%c%d%c%.2f"
        .zero   1
        .quad   15
2
On

Something along these lines, perhaps:

void AddFormatSpec(int, std::string& format) {
  format += "%d";
}
void AddFormatSpec(double, std::string& format) {
  format += "%g";
}
// Add overloads to taste.

template <typename... Ts>
void cpp_vsnprintf(char* s, size_t n, Ts&&... arg)
{
    std::string format;
    (AddFormatSpec(arg, format), ...);
    std::snprintf(s, n, format.c_str(), arg...);
}

Demo