Variadic Templates & concat

Español


Hello! A new post, this time, talking about Variadic Templates and a utility function that will be used on the Logger (which, will take more time to present that I thought).
 
Note, for those of you speaking in English, you can refer to here (Wikipedia), here (cplusplus.com) or here (MSDN) for articles on variadic templates. I’m doing the article on both languages for completions sake, but there are plenty of resources in English and I was more interested on doing it for the Spanish speakers.
 
Variadic templates are a new feature introduced in C++ with the C++11 standard. Their key point is to allow a template take a variable number of arguments. Its usage resembles variadic macros, but with typing and compilation instead of text preprocessing. The best way to see it in action is by an example.
 
I’ve used it to create a function that concatenates an arbitrary number of arguments into one string. If you ever used it, looks like the print function from Python 2.x (it also resembles a potential printf implementation). In terms of wrapping your head to the usage of variadic templates, I found useful to think of Haskell like recursion or list representations:

  • The base list is the empty list (depending on what you want to control, the base list can be the list with 1 element)
  • Any list contains a head which is a single element and a tail which is a list with the rest of the elements.

With this in mind, this is the utility function, used to concatenate (and, convert to string) any arbitrary object that implements the operator<< with an std::ostream.

namespace util {
    namespace string {
        template<typename... Args>
        inline std::string concat(Args&& ...args) {
            std::stringstream ss;
            concat_ss(ss, std::forward<Args>(args)...);
            return ss.str();
        }

First line after the definition of the namespaces is the declaration of the template arguments of the function. As you can see the ellipsis is added between the typename declaration and the new concept of Parameter Pack. Args, in this context represents the entire list of arguments this function will have.
 
The following line, the function signature, not only uses parameter packs but also universal references (&&) so to avoid unnecessary copies using another feature of C++11 called move sematics. It declares a function by the name concat that is templatized not only on the type of its arguments but also in its number.
 
Its body creates an std::stringstream object and then proceeds to call another function (see below) called concat_ss with the std::stringstream and the arguments, but note that the ellipsis are now on the right side of the args variable. From Wikipedia:

The ellipsis (…) operator has two roles. When it occurs to the left of the name of a parameter, it declares a parameter pack. Using the parameter pack, the user can bind zero or more arguments to the variadic template parameters. Parameter packs can also be used for non-type parameters. By contrast, when the ellipsis operator occurs to the right of a template or function call argument, it unpacks the parameter packs into separate arguments, […]. In practice, the use of an ellipsis operator in the code causes the whole expression that precedes the ellipsis to be repeated for every subsequent argument unpacked from the argument pack; and all these expressions will be separated by a comma.

That means if we call concat this way:

util::string::concat("The answer is ", 42, " or maybe ", 42.5);

it will possibly be compiled as:

        inline std::string concat(const char[]&& a1, int&& a2, const char[]&& a3, double&& a4) {
            std::stringstream ss;
            concat_ss(ss, std::forward<const char[]>(a1), 
                          std::forward<int>(a2), 
                          std::forward<const char[]>(a3), 
                          std::forward<double>(a4));
            return ss.str();
        }

You can safely ignore the universal references and the std::forward calls for now. Rest assure, are there for a reason, think of it as a way to telling “this object might be temporal or not, treat it accordingly, if it’s temporal don’t copy it from the caller to the callee (move semantics)” (it’s not exactly like this, but it’s good for now).
 
Now, moving on to that concat_ss function:

        template<typename Head, typename... Tail>
        inline void concat_ss(std::stringstream& ss, Head&& h, Tail&&... args) {
            ss << h;
            concat_ss(ss, std::forward<Tail>(args)...);
        }

Here comes the Haskell list mind set, defining the template as having 2 type arguments, Head as a standard one and Tail as another parameter pack, we are able to address the first parameter independently. This is done on the body, streaming the first parameter to the std::stringstream and calling itself again, but with one less element.
 
Where does it ends?

        template<typename Head>
        inline void concat_ss(std::stringstream& ss, Head&& h) {
            ss << h;
        }

Another version of concat_ss that has no tail, closes the recursion, and also ensures that concat can’t be called with no argument:
 
Avoid call to concat with no arguments.
 
Since calling concat with no arguments equals to calling concat_ss only with the std::stringstream the code fails to compile, since the only defined functions have exactly one extra parameter (second version) or more than one (first version).
 
Close the recursion
 
The call to concat_ss from concat defines a function with N arguments, which in turns calls another concat_ss function defined and receiving N-1 arguments. This process continues until N is equal to 2. Then, the first argument is streamed and the tail now is only a single element, making the match on the non-recursive and single-extra-argument version of concat_ss. Using the previous example:

  1. util::string::concat_ss(ss, "The answer is ", 42, " or maybe ", 42.5) will stream “The answer is ” and then call itself,
  2. util::string::concat_ss(ss, 42, " or maybe ", 42.5) will stream 42 and then call itself,
  3. util::string::concat_ss(ss, " or maybe ", 42.5) will stream ” or maybe ” and then call the non-recursive version of concat_ss that takes only one parameter, being in this case, 42.5.

I think that’s it, if you’re still reading and still interested in ‘move semantics’, this StackOverflow answer explains it in extreme detail. I might try to do a post myself, but I think its better explained by others as Stephan T. Lavavej (which gave input on SO) or Scott Meyers, maybe a good translation to Spanish will do 🙂
 
Look for the sources here. concat is part of the utilitary functions defined in util.h. As you can see, the code for the Logger is done, I just need to present a few concepts before presenting it by itself.
 
Cheers!

Back to Top




¡Hola! Nuevo artículo, esta vez, hablando sobre Variadic Templates y una función utilitaria que se va a usar en el Logger (el cual, va a tardar un poco más de tiempo en presentarse de lo que pensé).
 
Como comentaba en la sección en inglés, en ese idioma hay bastante más recurso, mejor calificado para aprender sobre el tema, pero en Español no vi tanto, por ello quise hacer un post sobre Variadic Templates :).
 
Son una nueva feature de C++ introducida con el estándar C++11. El punto clave es que permiten definir funciones/métodos/clases templatizadas utilizando un número variable de parámetros. Son como los Variadic Macros, pero fuertemente tipados y con compilación en lugar de un preprocesado de texto. La mejor manera de verlos en acción es con un ejemplo.
 
En este caso, creé una función que concatena un número arbitrario de parámetros en un solo string. Si alguna vez la usaste, es parecido a usar la función print de Python 2.x (también se podría decir que se parece a una posible implementación de printf). Para tratar de hacerse una idea de cómo usar los templates variádicos, yo encontré útil pensar en las listas de Haskell y la recursión:

  • La lista base, es la lista vacía (aunque dependiendo de qué quieras controlar, podría ser la lista con 1 solo elemento)
  • Cualquier lista contiene un head (Cabeza) que es un elemento individual y un tail (Cola) que es una lista con los elementos restantes.

Con eso en mente, esta es la función utilitaria, usada para concatenar (y, convertir a string) cualquier objeto arbitrario que implemente operator<< con un std::ostream.

namespace util {
    namespace string {
        template<typename... Args>
        inline std::string concat(Args&& ...args) {
            std::stringstream ss;
            concat_ss(ss, std::forward<Args>(args)...);
            return ss.str();
        }

Primera línea después de la definición de los namespaces es la declaración de los argumentos de template de la función. Como ves, los puntos suspensivos se agregan entre la declaración typename y el nuevo concepto de Parameter Pack. Args, en este contexto, representa la lista entera de argumentos que esta función va a tener.
 
La siguiente línea, la firma de la función, no solo usa “parameter packs” sino que además universal references (&&) para evitar copias innecesarias, usando otra feature de C++11 llamada move sematics. Declara una función con el nombre de concat que es un template no solo en el tipo de sus argumentos, sino también en su número.
 
En el cuerpo, crea un std::stringstream y procede a llamar otra función (más abajo) llamada concat_ss con el std::stringstream y los argumentos, pero en este caso, los puntos suspensivos están a la derecha de la variable args. Traducido de Wikipedia:

El operador puntos suspensivos (…) tiene 2 roles. Cuando aparece a la izquera del nombre de un parámetro, declara un parameter pack. Usando este parameter pack, el usuario puede ligar cero o más argumentos a los parámetros variádicos del template. Los parameter packs pueden usarse también para parámetros que no sean de tipos. En contraste, cuando el operador está a la derecha de un argumento de template o de llamada a función, desempaqueta el parameter pack en argumentos separados, […]. En la práctica, el uso del operador en el código, causa que toda la expresión que lo precede sea repetida por cada argumento desempaquetado del parameter pack; y todas esas expresiones serán separadas por comas.

Significa que si llamamos concat así:

util::string::concat("The answer is ", 42, " or maybe ", 42.5);

puede ser posiblemente compilada así:

        inline std::string concat(const char[]&& a1, int&& a2, const char[]&& a3, double&& a4) {
            std::stringstream ss;
            concat_ss(ss, std::forward<const char[]>(a1), 
                          std::forward<int>(a2), 
                          std::forward<const char[]>(a3), 
                          std::forward<double>(a4));
            return ss.str();
        }

Por ahora, a olvidarse de las universal references y de las llamadas a std::forward. Están por una razón, se podría interpretar como decirle al compilador “este objeto puede ser temporal o no, tratalo acorde, si es temporal no lo copies desde la función que llama a la que es llamada (move semantics)” (no es exactamente así, pero por ahora sirve).
 
Ahora, la función concat_ss:

        template<typename Head, typename... Tail>
        inline void concat_ss(std::stringstream& ss, Head&& h, Tail&&... args) {
            ss << h;
            concat_ss(ss, std::forward<Tail>(args)...);
        }

Y acá es donde viene la mentalidad de las listas de Haskell, definiendo el template con 2 argumentos de tipos, Head como uno tradicional y Tail como un parameter pack, es posible acceder y utilizar el primer parámetro de manera independiente. Lo que se hace en el cuerpo, enviándolo al std::stringstream y luego llamándose a sí misma nuevamente, pero con un elemento menos.
 
¿Dónde termina esto?

        template<typename Head>
        inline void concat_ss(std::stringstream& ss, Head&& h) {
            ss << h;
        }

Otra versión de concat_ss que no tiene cola, termina la recursión y además asegura que concat no se puede llamar sin argumentos:
 
Prevenir la llamada a concat sin argumentos.
 
Dado que llamar a concat sin argumentos equivale a llamar a concat_ss sólo con el std::stringstream el código falla al compilar, porque solo están definidas funciones que toman exactamente 1 parámetro extra (segunda versión) o más de uno (primera versión).
 
Fin de la recursión
 
La llamada a concat_ss desde concat define una función con N argumentos, que a su vez llama a otra versión de concat_ss definida y recibiendo N-1 argumentos. Este proceso continúa hasta que N es igual a 2. Momento en el cuál, el primer argumento se pasa al stream y la cola, que ahora es solo 1 elemento, matchea con la versión no-recursiva y con-un-argumento-extra de concat_ss. Usando el ejemplo anterior:

  1. util::string::concat_ss(ss, "The answer is ", 42, " or maybe ", 42.5) pasa al stream “The answer is ” y luego se llama a sí misma,
  2. util::string::concat_ss(ss, 42, " or maybe ", 42.5) pasa al stream 42 y luego se llama a sí misma,
  3. util::string::concat_ss(ss, " or maybe ", 42.5) pasa al stream ” or maybe ” y luego llama a la versión no-recursiva de concat_ss que recibe 1 solo parámetro extra, en este caso, 42.5.

Creo que es todo, si hay interés en seguir leyendo sobre ‘move semantics’, en inglés está esta respuesta de StackOverflow, explicando en extremo detalle. Como probablemente esté mejor explicado por personas como Stephan T. Lavavej (que dió input en esa respuesta de SO) o Scott Meyers, no creo que haga un artículo completo, quizás una traducción de alguno de los articulos originales al español baste 🙂
 
El código está acá. concat es parte de unas funciones utilitarias definidas en util.h. Como ves, el código para el Logger está completo, solo tengo que presentar algunos conceptos antes de presentarlo por sí solo.
 
¡Saludos!
Inicio

Advertisements

Leave a Reply (Deja una respuesta)

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s