Templated string formatting This function formats a string by replacing every occurrence of '{n}' with the corresponding argument that was passed to the function where n is an index that starts at 0. The same index can be used multiple times and the order is irrelevant. If n is out of bounds or cannot be converted to an integer number I just add '{n}' to the result. The function is intended to be used e.g with config files that can be changed by the user where it would be unsafe to use sprintf. The use of std::ostringstream allows me to pass any type that has an overloaded &lt;&lt; operator. I was considering using iterators in the loops but I'm not sure whether that would gain anything.

#pragma once

#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;sstream&gt;
#include &lt;string_view&gt;

namespace util::str{
    inline void to_string_vector(std::vector&lt;std::string&gt;&amp;){} //Dummy used to end recursion

    template&lt;typename T, typename ... Args&gt;
    void to_string_vector(std::vector&lt;std::string&gt;&amp; result, const T&amp; t, Args&amp;&amp; ...args){
        result.push_back((std::ostringstream{} &lt;&lt; t).str());
        to_string_vector(result, args...);
    }

    template&lt;typename ... Args&gt;
    std::string format(std::string_view fmt, Args&amp;&amp; ...args){
        std::string result;
        std::vector&lt;std::string&gt; values;

        result.reserve(fmt.size());
        to_string_vector(values, args...);

        for(std::size_t first = 0; first &lt; fmt.size(); ++first){
            if(fmt[first] == '{'){
                for(std::size_t second = first + 1; second &lt; fmt.size(); ++second){
                    if(fmt[second] == '}'){
                        try{
                            std::size_t index = std::stoi(std::string{fmt.substr(first + 1, second - first - 1)});

                            if(index &lt; values.size())
                                result += values[index];
                            else
                                result += fmt.substr(first, second - first + 1);
                        }catch(...){
                            result += fmt.substr(first, second - first + 1);
                        }

                        first = second;

                        break;
                    }else if(fmt[second] == '{'){
                        result += fmt.substr(first, second - first);
                        first = second - 1;

                        break;
                    }
                }
            }else{
                result.push_back(fmt[first]);
            }
        }

        return result;
    }
}
As I already said in the comments, some white space would be good to add, especially around { and }. Then I mentioned that you could possibly avoid creating the vector of strings, but well, that's not that easy:

template&lt;class... T&gt; struct pack {
    static_assert(sizeof...(T) == 0, "Unmatched argument data");
    static constexpr size_t count = 0;

    template&lt;class Action&gt;
    bool exec(size_t pos, Action&amp;&amp; action = Action()) {
        return false;
    }
};
template&lt;class T&gt; struct pack&lt;T&gt; {
    static constexpr size_t count = 1;
    template&lt;size_t I = 0&gt; using type = std::enable_if_t&lt;I == 0, T&gt;;
    T value;
    pack() = default;
    template&lt;class V&gt;
    pack(V&amp;&amp; value) : value(std::forward&lt;V&gt;(value)) {}

    template&lt;class Action&gt;
    bool exec(size_t pos, Action&amp;&amp; action = Action()) {
        if (pos &gt; 0) return false;
        action(value);
        return true;
    }
};
template&lt;class T, class... Next&gt; struct pack&lt;T, Next...&gt; {
    static constexpr size_t count = 1 + sizeof...(Next);
    template&lt;size_t I = 0&gt; using type = std::conditional_t&lt;I == 0,
        T, typename pack&lt;Next...&gt;::template type&lt;I == 0 ? 0 : I - 1&gt;&gt;;

    T value;
    pack&lt;Next...&gt; next;

    pack() = default;
    template&lt;class First, class... Args&gt;
    pack(First&amp;&amp; value, Args&amp;&amp;... args)
        : value(std::forward&lt;First&gt;(value))
        , next(std::forward&lt;Args&gt;(args)...) {}

    template&lt;class Action&gt;
    bool exec(size_t pos, Action&amp;&amp; action = Action()) {
        if (pos &gt;= count) return false;
        if (pos == 0) action(value);
        else next.exec(pos - 1, std::forward&lt;Action&gt;(action));
        return true;
    }
};
template&lt;class... Args&gt;
inline auto make_pack(Args&amp;&amp;... args) {
    return pack&lt;std::decay_t&lt;Args&gt;...&gt;(std::forward&lt;Args&gt;(args)...);
}
template&lt;class... Args&gt;
std::string format(std::string_view fmt, Args&amp;&amp; ...args) {
    auto pack = make_pack(std::forward&lt;Args&gt;(args)...);
    std::ostringstream result;
    for (std::size_t at = 0; at &lt; fmt.size();) {
        char c = fmt[at++];
        if (c == '{') {
            for (std::size_t start = at; at &lt; fmt.size();) {
                c = fmt[at++];
                if (c == '}') {
                    try {
                        std::size_t index = std::stoi(std::string{ fmt.substr(start, at - 1 - start) });

                        if (index &lt; pack.count)
                        {
                            pack.exec(index, [&amp;result](auto it) { result &lt;&lt; it; });
                            break;
                        }
                    }
                    catch (...) {
                    }
                    result &lt;&lt; fmt.substr(start - 1, at - start + 1);
                    break;
                }
                if (c == '{') {
                    result &lt;&lt; fmt.substr(start - 1, at - start + 1);
                    break;
                }
            }
        }
        else {
            result.put(c);
        }
    }
    return result.str();
}


I have also changed the indexing a bit, not that big of a difference.
The pack is in no way perfect (reduced version of my data pack) and pack.exec(index, [&amp;result](auto it) { result &lt;&lt; it; }); is not easy for the compiler to optimize, but the machine code I have observed was not bad (two arguments, release build).