As people in the comments have suggested, to help clean up formatting it would be better to use a proper formatting library like libfmt (which is also effectively in the C++20 standard now)
However, if your requirement is that you have a run-time std::vector<std::string>
and you intend to convert this to a compile-time variadic template argument sequence, then a library may not address this for you.
Since you are passing in a vector which contains a run-time value, there will -- at some point -- require a way to convert this into a compile-time list so that it can be passed to string_format
. This is possible, however it requires some infrastructure.
Since variadic arguments are a compile-time effect, we can't just convert args
to args...
easily. We must, at least, know the high-bound for how many functions we may convert to. However, this is still, at-least, possible using templates.
The idea is to build a jump-map (effectively, a switch-case) at compile-time, and reference into this at run-time. This will require that you have to set an arbitrary limit to the number of arguments that may be passed to string_format
however.
#include <string> // std::string
#include <vector> // std::vector
#include <cstdlib> // std::snprintf
#include <array> // std::array
#include <utility> // std::make_index_sequence
#include <cassert> // assert
template<typename ... Args>
std::string string_format(const std::string &format, Args... args) { ... }
template <std::size_t...Idxs>
std::string format_vector_impl(const std::string& format, const std::vector<std::string>& args, std::index_sequence<Idxs...>)
{
return string_format(format, args[Idxs].c_str()...);
}
template <std::size_t I>
std::string format_vector(const std::string& format, const std::vector<std::string>& args)
{
assert(I == args.size());
return format_vector_impl(format, args, std::make_index_sequence<I>{});
}
using converter = std::string(*)(const std::string&, const std::vector<std::string>&);
template <std::size_t...Idxs>
constexpr std::array<converter,sizeof...(Idxs)> make_converter_map(std::index_sequence<Idxs...>)
{
return {&format_vector<Idxs>...};
}
std::string test_format(const std::string& format, const std::vector<std::string>& args)
{
// let's make 31 arguments the arbitrary max
// Note: This is '32' here because '0' arguments is also a viable call
static constexpr auto s_limit = 32;
static constexpr auto s_converters = make_converter_map(std::make_index_sequence<s_limit>{});
assert(args.size() < s_limit); // if this ever gets triggered, change 'limit'
// use 'args.size()' as the index into the function
return s_converters[args.size()](format, args);
}
This solution assumes at least C++14 support, however it would work in C++11 support with some tweaks (requires a custom definition of std::index_sequence
)
The solution works as follows:
- Normalize all conversions to be just a signature of
std::string(*)(const std::string&, const std::vector<std::string>&)
via format_vector
format_vector
is templated on the max size of the vector
- Delegate
format_vector<I>
to format_vector_impl<Idxs...>
by creating a sequence from [0...I)
- In
format_vector
, use Idxs...
, a compile-time list of indexes, to unpack the argument vector
- Since all
format_vector
functions have the same signature, we can use function pointers to build an array. We build this at compile-time (constexpr
) and call into this at runtime.
- Find the correct handler by using
args.size()
in the array (effectively a jump-map).
You can make the number of arguments be as big as you want/need by using this approach -- but keep in mind that each new function will introduce more generated assembly code, which may bloat the executable.
Here's a working example on compiler-explorer
Note: Using a library like libfmt
would help address some undefined behavior your code currently has, wherein args...
is passing std::string
values to snprintf
. snprintf
uses C-style ...
variadics rather than variadic template
arguments, and so it unable to understand or use C++ types -- only primitive types like integral values and const char*
.
Edit: One other thing I just noticed is that your current code uses variable-length arrays (VLAs), which are not standard C++. Arrays need to be fixed at compile-time. This cannot be done at runtime in standard C++, but can be done with some compiler extensions. I'm referring to the string_format
function here:
size_t size = 1 + snprintf(nullptr, 0, format.c_str(), args ...);
char bytes[size] = {0};
For this, I recommend using either std::vector<char>
or even just std::string
and resizing with the computed size. Something like:
auto bytes = std::string{};
bytes.resize(size, ' ');