Create a C++ string using printf-style formatting
I agree with you that 'printf-style' formatting was by many aspects better than C++'s manipulators: more concise, more varied, etc. That said, I feel like it's a step backwards if we port them into C++ without upgrading the C logic behind it. In my opinion, your code fails to do that for two reasons: lack of automatic memory management, and lack of type safety.

About type safety, I believe we now must use variadic templates when dealing with an unknown number of arguments. Variadic macros have too many shortcomings for us to continue using them. With variadic templates, we're able to verify if format directives and arguments match in number and types in a more reliable way (although I agree that when you can rely on a format chain the risk is manageable even with C-like macros).

About memory management: I believe we must avoid explicit memory management whenever we can. std::stringstream is a good way to do that when one wants to build a string progressively from heterogeneous arguments.

So here's an embryo of what I would consider more modern C++:

#include &lt;string&gt;
#include &lt;sstream&gt;
#include &lt;iostream&gt;
#include &lt;type_traits&gt;
#include &lt;exception&gt;

// base case of recursion, no more arguments
void format_impl(std::stringstream&amp; ss, const char* format) { 
    while (*format) {
        if (*format == '%' &amp;&amp; *++format != '%') // %% == % (not a format directive)
            throw std::invalid_argument("not enough arguments !\n");
        ss &lt;&lt; *format++;
    }
}

template &lt;typename Arg, typename... Args&gt;
void format_impl(std::stringstream&amp; ss, const char* format, Arg arg, Args... args) {
    while (*format) {
        if (*format == '%' &amp;&amp; *++format != '%') {
            auto current_format_qualifier = *format;
            switch(current_format_qualifier) {
                case 'd' : if (!std::is_integral&lt;Arg&gt;()) throw std::invalid_argument("%d introduces integral argument");
                // etc.
            }
            // it's true you'd have to handle many more format qualifiers, but on a safer basis
            ss &lt;&lt; arg; // arg type is deduced
            return format_impl(ss, ++format, args...); // one arg less
        }
        ss &lt;&lt; *format++;
        } // the format string is exhausted and we still have args : throw
    throw std::invalid_argument("Too many arguments\n");
}

template &lt;typename... Args&gt;
std::string format(const char* fmt, Args... args) {
    std::stringstream ss;
    format_impl(ss, fmt, args...);
    return ss.str();
}

int main() {
    auto display = format("My name is %s and I'm %d year old", "John", 22);
    std::cout &lt;&lt; display;
}