Auto + expression templates = Undefined Behavior?

Una de las características más interesantes de C++11 es la inferencia de tipos, gracias a la palabra clave auto.

Auto indica al compilador que utilice como tipo de una variable el tipo de la expresión de su inicialización. Por ejemplo:


int foo() { return 12; }

int main()
{
	auto result = foo(); //El tipo de result es int.
}

Esto permite que el código sea mucho más mantenible, ya que si no se declara explícitamente el tipo de la variable, sino que se usa el de la inicialización, al cambiar la implementación de la inicialización no ocurre nada. Veamos un ejemplo:


/*
* Tenemos como ejemplo ésta factoría.
* La versión original utiliza raw-pointers:
*/
Foo* create_foo();

/*
* Pero decidimos dejar de usar raw-pointers, ya que son propensos a errores: ¿Quien se encarga
* de la destrucción de la instancia? La interfaz que utiliza raw-pointers no lo deja nada claro.
* Por ello pensamos que sería mejor usar std::unique_ptr. Así indicamos al usuario que la instancia 
* se destruye cuando él deje de usarla. Además no le permitimos que la copie:
*/
std::unique_ptr<Foo> create_foo();

La ventaja de auto consiste en que si el usuario inicializa las instancias de Foo utilizando auto en lugar del tipo explícito de la factoría, cuando hacemos la refactorización no tendrá que modificar su código; ya que el compilador se encarga de inferir el tipo correcto. Y la interfaz de un smart pointer es idéntica a la de un raw pointer. En cambio, si hubiera definido una instancia de Foo como Foo* my_foo_instance = create_foo();, se vería obligado a cambiar el tipo de la variable.

Todo parece maravilloso, hasta que encuentras con un pequeño problema: ¿Que pasa con las expression templates?

Las expression templates son un mecanismo por el cual se utilizan plantillas para representar una expresión. Esto era especialmente útil para eliminar la creación de objetos temporales en expresiones muy largas, tales como std::string result = a + b + c + d; . “Eran” porque con las move semantics de C++11, esto ya no es estrictamente necesario, ya que si la sobrecarga de operator+ y el operador de asignación admiten rvalues, no tiene por qué generarse ningún string temporal. Se utiliza el mismo durante toda la expresión.
Aún así existen casos en los que siguen siendo útiles. Por ejemplo, para hacer que las operaciones aritméticas con matrices sean cache-friendly, y por tanto más eficientes. En “C++ Template Metaprogramming”, capítulo 10.5.1 se da una muy buena explicación de éste tema.
Como he dicho, las template expressions utilizan templates para representar expresiones. Esto significa que, utilizando el ejemplo de la string, la expresión “a + b + c + d” no devuelve string, sino que devuelve una plantilla del estilo de string::concat<string::concat<string::concat>>;. Por supuesto, dicha plantilla es convertible a string, y es precisamente ahí donde se realiza la “magia”.
La gracia está en que al convertir implícitamente la template expression al tipo del resultado (String en éste caso), el usuario no tiene por qué saber que la concatenación utiliza una template expression. El mecanismo es completamente trasparente. Por ello, normalmente el proxy (La template que representa la expresión) se implementa como una clase interna privada a la clase en cuestión, String en nuestro ejemplo. Así el usuario no puede acceder al proxy.

Así pues, si la clase string implementara la concatenación utilizando template expressions, la sentencia siguiente es completamente válida, y como he dicho, totalmente trasparente para el usuario:


string result = a + b + c + d;

Pero, ¿que pasa si utilizamos la inferencia de tipos?:


auto result = a + b + c + d;

Como dijimos en un principio, auto indica al compilador que infiera el tipo de una variable a partir del tipo de la expresión que la inicializa. ¿Y cual es el tipo de esa concatenación?: string::concat<string::concat<string::concat>> , que es privado. Anda, y ¿ahora que hacemos?

Lo más gracioso es que esto no genera ningún error de compilación (What?, pero si hemos quedado que concat es privado!), sino que durante la ejecución genera undefined behaviour. ¿Como? ¿Pero si poniendo string funciona perfectamente, por qué va a ser undefined behaviour usando auto?
Veamos: La expression template guarda referencias a los operandos izquierdo y derecho de la expresión original, para poder realizar la operación. Por ejemplo, “a+b+c” se traduce a una template expression de tipo string::concat<string::concat>:


class string
{
	template
	class concat
	{
		const T&amp; left;
		const string&amp; right;
	};
};

Es decir, la expression template guarda una referencia a la subexpresión a su izquierda, y una referencia al operando a su derecha. Y por tanto la template expression que representa la expresión completa es como una “lista recursiva” de las subexpresiones que la forman.
Al utilizar la versión con string, llama a operator string(), realiza la concatenación devolviendo la cadena resultante, y todos contentos.
Pero, cuando usas auto, el tipo de la variable no es string, también es una expression template, y por tanto no se realiza conversión y no se genera ningún nuevo string con el resultado. Por supuesto, al terminar la sentencia “a + b + c + d” se elimina la template expression (RAII). Pero como ya hemos asignado el resultado a una variable (Hemos copiado la expresión resultante en una variable), no hay problema. Pues si, hay un gran problema: La expresión guarda referencias. Referencias a las subexpresiones que acaban de ser eliminadas. Por tanto, al usar la variable, undefined behavior (Estás accediendo a una referencia a un objeto que ha sido eliminado).

Fuente: http://lanzkron.wordpress.com/2011/02/21/inferring-too-much/

PD: He de decir que me ha costado bastante entender donde estaba el problema, no era algo especialmente sencillo. Me ha llamado la atención la sugerencia de un posible operator auto(). Tiene sentido que si ya nos dejan hacer conversiones implícitas en runtime, nos dejen también hacerlas en tiempo de compilación (Sería algo así como: “Trata a esta clase como si fuera ésta otra”).
Me recuerda a cierta pregunta que hice en stackoverflow hace algún tiempo. Este “operator auto()” podría solucionar el problema.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s