boost::format with variadic template arguments
Asked Answered
S

3

11

Suppose I have a printf-like function (used for logging) utilizing perfect forwarding:

template<typename... Arguments>
void awesome_printf(std::string const& fmt, Arguments&&... args)
{
    boost::format f(fmt);
    f % /* How to specify `args` here? */;
    BlackBoxLogFunction(boost::str(f).c_str());
}

(I didn't compile this but my real function follows this guideline)

How can I "unroll" the variadic argument into the boost::format variable f?

Superaltar answered 16/9, 2014 at 2:11 Comment(5)
I don't know if it will work, but have you tried e.g. args...?Geophysics
@JoachimPileborg I did try that: coliru.stacked-crooked.com/a/9e651d5f7532cc67 , it doesn't work unfortunately (unless I'm doing it wrong).Superaltar
That's how you expand variadic template arguments. Unfortunately Boost format uses overloaded % operator to separate arguments, which will not work with expanded argument packs.Geophysics
@JoachimPileborg Well, it was designed before template parameter-packs and perfect forwarding existed, which accounts for a good part of the design (and its weak spots).Kenney
I suppose it could be done with the fold operators that come with C++1z. en.cppreference.com/w/cpp/language/fold . Related : #27583362Philip
S
11

I did some googling and found an interesting solution:

#include <iostream>
#include <boost/format.hpp>

template<typename... Arguments>
void format_vargs(std::string const& fmt, Arguments&&... args)
{
    boost::format f(fmt);
    int unroll[] {0, (f % std::forward<Arguments>(args), 0)...};
    static_cast<void>(unroll);

    std::cout << boost::str(f);
}

int main()
{
    format_vargs("%s %d %d", "Test", 1, 2);
}

I don't know if this is a recommended solution but it seems to work. I don't like the hacky static_cast usage, which seems necessary to silence the unused variable warnings on GCC.

Superaltar answered 16/9, 2014 at 2:43 Comment(6)
You can avoid the cast by not creating a temp variable using unroll = int[]; unroll{0, (f % std::forward<Arguments>(args), 0)...};Debora
@Debora That works out nicely; however the using seems superfluous... I wonder why int[] { /* ... */ }; doesn't work...Superaltar
Because the grammar doesn't allow it. It's either the using or the cast.Debora
Note that this trick is dependent on f % x % y; being equivalent to f % x; f % y;. (Also, I'd cast f % std::forward<Arguments>(args) to void just in case the type has an overloaded comma operator.)Stenograph
You may also use std::initializer_list<int>{(f% forward<Args>(args), 0)...}; to avoid the cast (and the case without args)(but requires an include).Gaven
Is std::forward needed in the expansion expression? What would be the difference between using it and specifying args directly?Superaltar
P
14

Just to summarize the void.pointer's solution and the hints proposed by Praetorian, T.C. and Jarod42, let me provide the final version (online demo)

#include <boost/format.hpp>
#include <iostream>

template<typename... Arguments>
std::string FormatArgs(const std::string& fmt, const Arguments&... args)
{
    boost::format f(fmt);
    std::initializer_list<char> {(static_cast<void>(
        f % args
    ), char{}) ...};

    return boost::str(f);
}

int main()
{
    std::cout << FormatArgs("no args\n"); // "no args"
    std::cout << FormatArgs("%s; %s; %s;\n", 123, 4.3, "foo"); // 123; 4.3; foo;
    std::cout << FormatArgs("%2% %1% %2%\n", 1, 12); // 12 1 12
}

Also, as it was noted by T.C., using the fold expression syntax, available since C++17, the FormatArgs function can be rewritten in the more succinct way

template<typename... Arguments>
std::string FormatArgs(const std::string& fmt, const Arguments&... args)
{
    return boost::str((boost::format(fmt) % ... % args));
}
Programme answered 8/9, 2017 at 19:41 Comment(3)
Wow, I love the fold expression... I didn't even know they added that. Awesome. Question for clarity: Is the inner parenthesis required in your fold expression solution? In other words, would this work: boost::str(boost::format(fmt) % ... % args);Superaltar
I expanded your demo to include your fold expression solution: coliru.stacked-crooked.com/a/bfabe441fba08d62 I also tried removing those inner parenthesis but it doesn't compile. Would be educational to know why, though. The diagnostic doesn't help much.Superaltar
@void.pointer, it seems that parenthesis around a fold expression is a mandatory part of it and can't be omitted. You can look at the grammar description of fold expression ("Explanation" section). When the fold expression expands, parenthesis around it are removed, so in case of boost::str((fold expr)), we need to have extra parenthesis to get boost::str(expanded fold expr) after the fold expression expandsProgramme
S
12

As is usual with variadic templates, you can use recursion:

std::string awesome_printf_helper(boost::format& f){
    return boost::str(f);
}

template<class T, class... Args>
std::string awesome_printf_helper(boost::format& f, T&& t, Args&&... args){
    return awesome_printf_helper(f % std::forward<T>(t), std::forward<Args>(args)...);
}

template<typename... Arguments>
void awesome_printf(std::string const& fmt, Arguments&&... args)
{
    boost::format f(fmt);

    auto result = awesome_printf_helper(f, std::forward<Arguments>(args)...);

    // call BlackBoxLogFunction with result as appropriate, e.g.
    std::cout << result;
}

Demo.


In C++17, simply (f % ... % std::forward<Arguments>(args)); will do.

Stenograph answered 16/9, 2014 at 2:36 Comment(1)
Forcing the newline at the end of the output is not a good idea for reusability: It's easily integrated into the format-string, and making it mandatory precludes extending the line before it ends.Kenney
S
11

I did some googling and found an interesting solution:

#include <iostream>
#include <boost/format.hpp>

template<typename... Arguments>
void format_vargs(std::string const& fmt, Arguments&&... args)
{
    boost::format f(fmt);
    int unroll[] {0, (f % std::forward<Arguments>(args), 0)...};
    static_cast<void>(unroll);

    std::cout << boost::str(f);
}

int main()
{
    format_vargs("%s %d %d", "Test", 1, 2);
}

I don't know if this is a recommended solution but it seems to work. I don't like the hacky static_cast usage, which seems necessary to silence the unused variable warnings on GCC.

Superaltar answered 16/9, 2014 at 2:43 Comment(6)
You can avoid the cast by not creating a temp variable using unroll = int[]; unroll{0, (f % std::forward<Arguments>(args), 0)...};Debora
@Debora That works out nicely; however the using seems superfluous... I wonder why int[] { /* ... */ }; doesn't work...Superaltar
Because the grammar doesn't allow it. It's either the using or the cast.Debora
Note that this trick is dependent on f % x % y; being equivalent to f % x; f % y;. (Also, I'd cast f % std::forward<Arguments>(args) to void just in case the type has an overloaded comma operator.)Stenograph
You may also use std::initializer_list<int>{(f% forward<Args>(args), 0)...}; to avoid the cast (and the case without args)(but requires an include).Gaven
Is std::forward needed in the expansion expression? What would be the difference between using it and specifying args directly?Superaltar

© 2022 - 2024 — McMap. All rights reserved.