Templated string formatting
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).