Search code examples
c++c++17variadic-templatescstdio

Wrap cstdio print function with C++ variadic template


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?


Solution

  • 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