C++11 compile time format string literal construction for invoking printf
Asked Answered
S

2

7

What I would like to do is to create:

template<Args... args)>
int println(Args...) {
  // implementation which calls:
  // printf("<string literal format string at compile time>", args...);
  // additional perk would be compile time type checking
  // I expect to provide a format string for each type by some template
  // specialization.
}

I have been analyzing two interesting pieces of work with compile time string literals:

Basically I am more or less able to statically infer length of required string literal for format but nothing more as compiler refuses to treat my work as constexpr. Another big problem is when using constexpr strings from the above link the compiler is never able to infer the size of the resulting string.

The more I try to achieve this the more my ignorance outweighs my enthusiasm. I would appreciate any tips and/or code samples that solve some or all of the problems with doing such an implementation.

Note: I am not looking for advice regarding using different forms of logging such as through cout.

Note2: The should not use any std::string as those are runtime

Note3: Generally all the type-safe printfs are using different approaches and I am familar that this could be easily done with multiple calls to printf. I would like to do it in a single call. I also know that the buffer could be incrementally built but this question is about constructing the format string. :)

Update: Basically what part of the code needs to achieve is

constexpr const char* formatString = build_compile_time_format_string(args...);
// build_compile_time_format_string(3, "hi", -3.4)
//     should evaluate to "%d %s %f"
//          or to "%d hi %f"
Satanism answered 10/6, 2014 at 22:44 Comment(13)
gist.github.com/souravdatta/ac0321f29ac2b379fce2 or codeproject.com/Articles/514443/…Subcontraoctave
Do you mean a string literal that can be either char or wchar_t? If so, look at goo.gl/nS76aW.Expansion
@BenKey Your URL is not working unfortunately.Satanism
@Alex: This is run-time as it is using std::string, I am looking for compile time format string construction for printf, I have updated the question to be a bit more explicit.Satanism
@Satanism maybe this helps then? akrzemi1.wordpress.com/2011/05/11/… so in essense you want to do the string tokenization and the zipping with another string at compile time right? (I really wonder though if this isn't a bit of overkill)Subcontraoctave
@dyp: That's parsing a printf format string, wheras this one is creating one.Chihuahua
@RushPLL I assume for the example you want formatString(3, "hi", -3.4) to evaluate to "%d %s %f"?Chihuahua
@dyp: Your link is interesting but I want to not write the format string at all, I want it constructed automatically from argument list. Something like javascript console.log("Hello", a, b, "c"). I know it could be easily done with multiple calls to printf but I want only one. :)Satanism
@Satanism I really wonder why you want to do it this way? my first posting avoids creating a format string and either puts it to a string buffer or writes it to a fd. (depending on how you implement it) and actually it is faster if you avoid using a format string in the first place.Subcontraoctave
typesafe printf? abel.web.elte.hu/mpllibs/safe_printf/index.htmlVitale
@Alex: Well basically I would like to use it on an embedded platform (4KB-32KB of RAM) as a safe replacement for printf and ensure the whole thing is pushed in a single call and a single line. I know there may be other solutions or workarounds such as building the buffer incrementally. I may choose the other solutions if this question is not answered. Anyway, I find this an interesting problem ...Satanism
How do you make a difference in your scheme between a constant string and "%s"? (see the diff between you update and @MooingDuck comment).Vitale
@Satanism look at how FreeBSD implemented printf: svnweb.freebsd.org/base/head/lib/libc/stdio/… parsing at compiletime is definitely interesting. I just wanted to point out, that the string is still parsed at runtime. and even if you push it at one go (on a serial line, the lines will still mix up as one printf will be resoled to mulple writes.Subcontraoctave
C
7

Simple enough, we'll build a compile time string with a " %d" or whever for each type, concatenate a '\n', and strip the leading space.

To start, we needed a type to use as a compile time string:

template<char...cs> struct compile_time_string 
{static constexpr char str[sizeof...(cs)+1] = {cs...,'\0'};};
template<char...cs>
const char compile_time_string<cs...>::str[sizeof...(cs)+1];

And to prevent intermediate steps from generating pointless buffers, a stringbuilder:

template<char...cs> struct compile_time_stringbuilder 
{typedef compile_time_string<cs...> string;};

//remove leading spaces from stringbuilder
template<char...cs> struct compile_time_stringbuilder<' ', cs...>
{typedef typename compile_time_stringbuilder<cs...>::string string;};

Then, you need functions that take a compile_time_stringbuffer and a type, and return a compile_time_stringbuffer with the " %d" or whatever appended. Since we're dealing with types, I don't even bother defining the functions. Note that my "end" specialization concatenates a '\n' character for you

template<char...cs, class...Ts> 
compile_time_stringbuilder<cs...,'\n'> concatenate_compile_time_format_string(compile_time_stringbuilder<cs...>);

template<char...cs, class...Ts> 
auto concatenate_compile_time_format_string(compile_time_stringbuilder<cs...>,int,Ts...args)
-> decltype(concatenate_compile_time_format_string(compile_time_stringbuilder<cs...,' ','%','d'>(),args...));

template<char...cs, class...Ts> 
auto concatenate_compile_time_format_string(compile_time_stringbuilder<cs...>,const char*,Ts...args)
-> decltype(concatenate_compile_time_format_string(compile_time_stringbuilder<cs...,' ','%','s'>(),args...));

template<char...cs, class...Ts> 
auto concatenate_compile_time_format_string(compile_time_stringbuilder<cs...>,double,Ts...args)
-> decltype(concatenate_compile_time_format_string(compile_time_stringbuilder<cs...,' ','%','f'>(),args...));

Finally, a helpful, easy to use interface.

template<class...Ts>
constexpr const char* build_compile_time_format_string()
{
    using compile_time_stringbuilder = decltype(concatenate_compile_time_format_string(compile_time_stringbuilder<>(),std::declval<Ts>()...));
    using compile_time_string = typename compile_time_stringbuilder::string;
    return compile_time_string::str;
}

And it's used like so:

template<class...Args>
void println(Args...args) {
    constexpr const char* formatString = build_compile_time_format_string<Args...>();
    std::cout << formatString;
}

Here's proof of execution: http://coliru.stacked-crooked.com/a/16dc0becd3391aaa


Completely unnecessarily, it might be fun to flesh out compile_time_string to roughly match the interface of const std::string, along these lines:

template<char...cs> struct compile_time_string 
{
    static constexpr char str[sizeof...(cs)+1] = {cs...,'\0'};
    constexpr size_t size() {return sizeof...(cs);}
    constexpr char* begin() {return str;}
    constexpr char* end() {return str+sizeof...(cs);}
};
Chihuahua answered 11/6, 2014 at 0:57 Comment(14)
Great work, though one thing is missing constexpr: template<char...cs> constexpr const char compile_time_string<cs...>::str[sizeof...(cs)+1];. Without this G++ would not work and probably something was being done at runtime.Satanism
@Satanism I wrote this after reading your question, thought it might be of interest. It's a completely different way of implementing the above, with increased dynamics.Loehr
@FilipRoséen-refp: indeed very nice, it allows for more streamlined declaration of formats per types.Satanism
@FilipRoséen-refp: Can you post your solution as secondary answer for posterity and so that I can up your answer?Satanism
@Mooing Duck: Do you think it is possible to merge any constant string arguments into the format itself? So passing println("Hello", "world"") would actually produce format Hello world\n and not %s %s\n ?Satanism
Same question goes to @FilipRoséen-refp whose solution is a bit concise :)Satanism
@Satanism only through the use of a MACROLoehr
@RushPL: I fail to see a need: println("Hello" " " "world") does what you want already.Chihuahua
@FilipRoséen-refp: In my sample, the compile_time_string<...>::str buffer is not constexpr, and is in the C++ model probably filled in at startup. Optimizations may make it a literal at compile time, but that's an optimization, not affecting the model.Chihuahua
@MooingDuck may I ask why you think it's relevant to PING me with that "info"?Loehr
@Mooing Duck: I am beginning to turn those techniques into a useful hpp file I wish to later publish so improving it even if by a tiny bit is a cool exercise imho.Satanism
Besides, println("Hello", 5, "world") wouldn't do the optimal thing.Satanism
@Satanism I agree that if the variable is known at compile time it is more ideal to the variable into the format string. I am curious after 4 years whether you have a better solution to share. The solutions provided by Mooning Duck and FilipRoséen-refp are very impressive!Landlocked
@Landlocked unfortunately no but watching compile time Regexps talk from CppCon convinces me that it should be possible!Satanism
F
1

Here's my take. I allowed myself to copy a bit from @Filip Roséen's Answer:

#include <iostream>

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>
constexpr auto build_compile_time_format_string(Ts... args)
{
    return cxpr_string<Ts...>::value;
}

int main()
{
    constexpr const char* formatString = build_compile_time_format_string(3, "hi", -3.4);
    std::cout << formatString << std::endl;
}

Demo

Output:

%d%s%.2f

I stumbled upon this while creating a variadic format string composer for snprintf.

Footlights answered 6/6, 2022 at 20:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.