Variadic template make_CString for MFC's CString

  • Posted on: 27 February 2016

For reasons beyond my control, I often work with a codebase that makes use of Microsoft MFC's string class, CString. CString has its detractors and proponents. I don't have a whole lot to add to the debate that hasn't been said already. Instead, I'm going to focus on using C++ variadic templates to get around one of my least favorite CString "anti-idioms". Since I'm not overly familiar with variadic templates, I plan to go over them in slow-mo. If you're in that boat also, this should be a good first tutorial.

Anyway, here's the idiom I'm looking to avoid:

CString foo;
foo.Format("%g %g", mycomplex.real(),mycomplex.imag());
std::cout << foo; // (or anything that uses foo).

Well, it's not terrible, but it is annoying after enough times. One would hope for some sort of CString "format constructor" -- no such luck. It would even be okay if CString::Format returned a reference to itself.

Luckily, using variadic templates, we can pretty easily write a function template make_CString(const std::string& fmt, Args... args) that we can use like this:

std::cout << make_CString(  ("%g %g", mycomplex.real(),mycomplex.imag()) ); // that's it!

Let's look at the template, and then discuss it:

template<typename... Args>
CString make_CString(const std::string& fmt, Args... args)
{
    int sz = std::snprintf(nullptr, 0, fmt.c_str(), args...);
    std::vector<char> buf(sz + 1); // note +1 for null terminator
    std::snprintf(&buf[0], buf.size(), fmt.c_str(), args...);

    std::string s(&buf[0],buf.size());
    return CString(s.c_str());
}

The declaration, template<typename... Args> CString make_CString(const std::string& fmt, Args... args) basically says something like: the first argument to this function is a constant std::string reference, and the remaining args, which of are of any type, are represented by args, which is called a "parameter pack".

In the function, we use args... to represent the parameter pack. This parameter pack can be passed to a function where the parameter pack "makes sense". For example, if we called this function like make_CString("%g",1.4), we could call cos(args...) in the function. We can also call another variadic template which accepts a parameter pack.

It's a bit tricky to explain, but once you hack around with it, you'll start to get the idea.

I adapted the meat of the template from cppreference.com. snprintf is basically a safer version of the classic sprintf. It is guaranteed not to write beyond the specified buffer, and can also be used to find how many bytes are needed for said specified buffer.

This template provides a workaround for the CString anti-idiom, and we wrote a real, live variadic function template, but we still have some issues to take care of. For one, we're still using a string formatter, which is bug prone (one wrong formatter, and you have a crash on your hands). Secondly, in a perfect world, make_CString could deal with non-POD types.

Recursive Variadic Templates

A few moments ago, I noted that we used the template argument Args... args to represent "the rest of the supplied args.", and we used args... to represent the parameter pack in the function. Putting these together, we can create a recursive variadic template, like this:

template<typename T, typename... Args>
void strstr_append(std::stringstream& ss, const T& t, const Args&... args)
{
    ss << t << " ";
    strstr_append(ss,args...);
} 

This example recursively adds const T& t to stringstream ss, which eventually adds each args... to the stringstream.

When we ask to the compiler to handle strstr_append(ss,args...), the compiler figures out that it must use the first parameter of args... as const T& t, and so the parameter pack const Args&... args gets one parameter smaller each time. Eventually, the compiler looks for something like void strstr_append(std::stringstream& ss, const T& t), which we don't have! Thus, to complete this recursive variadic template idea, we have to define the base case:

template<typename T>
void strstr_append(std::stringstream& ss, const T& t)
{
    ss << t;
}

Note that this template must be def'd before the "normal" recursive template!

Back to CString

You might have guessed where we're going with this. We only have to add one more function to invent some version of make_CString that can deal with other types, as long as all these types have std::ostream& operator<<(std::ostream& os, <the type>& t). For now, let's call it make_Cstring2:

template<typename... Args>
CString make_CString2(const Args&... args)
{
    std::stringstream ss;
    strstr_append(ss,args...);
 
    return CString( ss.str().c_str() );

}

To give this some substance, say we have this class:

class Thing
{
    public:
    Thing(double kk, const std::string& yy):
        k(kk),y(yy)
    {
       std::cout << "making a thing\n";
    }

    Thing(const Thing& other):
        k(other.k),y(other.y)
    {
       std::cout << "copying a thing\n";
    }

    public:
    double      k;
    std::string y;
};


std::ostream& operator<<(std::ostream& os, const Thing& t){
    os << "k " << t.k << ", ";
    os << "y " << t.y << " ";
    return os;
}

Then we can:

Thing t(6,"hello");
std::cout << make_CString2(t,std::complex<double>(1.0,1.0),5,"str");

Okay, now we can create a MFC CString from any number of any kind of type we want, and, as a bonus, we rid ourselves of the always-dangerous format string. Hopefully, we also have some idea how to work with recursive variadic templates.

What's Next?

You may have noticed that while introducing/explaining make_Cstring2, I changed Args... args to const Args&... args. What's the reason?

In a word, we do this to avoid copying some of args... for each recursive instantiation of strstr_append. To see this, consider the fact that Args... args says "pass by value", and try to write out the recursive function that the compiler creates.

We are lucky enough that for this example, adding the arg. to the stringstream is indeed a const operation. When that's not the case (say we want to modify one of the args at some point), we turn to universal references and std::forward, which is a wormhole for another day!