1

I really don’t want to write such ugly code, if you have a better way, please help me optimize it. I really don't want to write more case for vector size, is very stupid.

template<typename ... Args>
std::string string_format(const std::string &format, Args... args)
{
    size_t size = 1 + snprintf(nullptr, 0, format.c_str(), args ...);

    char bytes[size] = {0};
    snprintf(bytes, size, format.c_str(), args ...);
    return std::string(bytes);
}

std::string test_format(const std::string& str, const std::vector<std::string>& vStr)
{
    switch (vStr.size())
    {
    case 1:
        return string_format(str, vStr[0]);
        break;
    case 2:
        return string_format(str, vStr[0], vStr[1]);
        break;
    case 3:
        return string_format(str, vStr[0], vStr[1], vStr[2]);
        break;
    case 4:
        return string_format(str, vStr[0], vStr[1], vStr[2], vStr[3]);
        break;
    case 5:
        return string_format(str, vStr[0], vStr[1], vStr[2], vStr[3], vStr[4]);
        break;
    case 6:
        return string_format(str, vStr[0], vStr[1], vStr[2], vStr[3], vStr[4], vStr[5]);
        break;
    // ....

    default:
        return str;
        break;
    }
}
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
s f
  • 107
  • 5
  • 1
    What are you trying to do? – Aykhan Hagverdili Jun 07 '20 at 13:42
  • 1
    Do you know how to use formatted output functions from the C++ library? You are trying to use C formatting in C++ code. C++ code has its own formatted output functions that do not require allocating anything. You will find more information in your C++ textbook. – Sam Varshavchik Jun 07 '20 at 13:42
  • 1
    I don't know exactly what you want to do but you may be interested in this: [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt) if your compiler does not have it yet. – drescherjm Jun 07 '20 at 13:50
  • I you want to use C like formating you can use `boost::format` library. – Jean-Marc Volle Jun 07 '20 at 13:53
  • Careful, your current code exhibits undefined behavior. `snprintf` is only defined to work primitive types since it uses a variadic arguments list, but `args...` is actually passing in `std::string` arguments. For `std::string` arguments, you need to take the `c_str()` of each argument so that it's a `const char*` type. As other people are mentioning, it might be better to use a pre-canned formatting library since it can take into account the type-safety. – Human-Compiler Jun 07 '20 at 14:04
  • Is there a reason that `test_format` takes a `std::vector`, rather than calling into the `string_format` function directly? Is it a requirement that the arguments to the format come as a vector of strings? If the vector is a requirement, then using a proper formatting library may not help – Human-Compiler Jun 07 '20 at 14:13
  • Does this answer your question? [How can I format a std::string using a collection of arguments?](https://stackoverflow.com/questions/5076472/how-can-i-format-a-stdstring-using-a-collection-of-arguments) – xskxzr Jun 07 '20 at 14:37

1 Answers1

3

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, ' ');
Human-Compiler
  • 11,022
  • 1
  • 32
  • 59