miércoles, 27 de abril de 2016

Entendiendo la sintaxis move

Una de las novedades de C++11 es la sintaxis move. Esta nueva característica de C++ pretende acelerar el trasvase de información entre elementos. Como toda herramienta conviene aprender a utilizarla para ser capaces de exprimir todos su potencial.

Para comprender lo que se comenta en esta entrada es necesario entender los conceptos de Rvalue y Lvalue, lo cuales se explican en esta otra entrada.

¿Qué es la sintaxis move?

La sintaxis move vino para suplir una de las grandes carencias de C++ en cuanto al rendimiento en tiempo de ejecución. Antes de la llegada de C++11 los elementos de tipo Rvalue se consideraban objetos no modificables, lo que impide realizar cambios sobre ellos.

La llegada del nuevo estándar trajo bajo el brazo una forma de cambiar este comportamiento. C++11 añadió un nuevo constructor y un nuevo operador de asignación a la lista de los ya existentes:

class POO
{
  // Constructor move
  POO(POO&&);

  // Operador de asignación move
  POO& operator=(POO&&);
};

El argumento de estas funciones, como se puede ver, se caracteriza por el doble ampersand. Además, dicho parámetro no debe ser constante. Ya que nuestro objetivo es poder realizar cambios en el objeto Rvalue no parece buena idea etiquetar dicho objeto como const en nuestra función.

Estas dos funciones, si están definidas, se llamarán automáticamente cuando le pasemos como parámetro un objeto de tipo Rvalue y el motivo que da sentido a su existencia es que, dado que los Rvalue dejan de existir tras la ejecución de la sentencia, estas funciones pueden apropiarse los recursos adquiridos por la clase original como suyos en vez de realizar una costosa copia:

class ObjetoPesado
{
  private:

    const int NumDatos = 1000000;
    int* _datos;

  public:

    ObjetoPesado()
      : _datos(new int[NumDatos])
    {
    }

    ObjetoPesado(ObjetoPesado&& origen)
      : _datos(nullptr)
    {
      std::swap(_datos,origen._datos);
    }

    ObjetoPesado(const ObjetoPesado& origen)
      : ObjetoPesado() // Llamada al constructor por defecto,
                       // soportado desde C++11
    {
      std::copy(origen._datos,
                origen._datos+NumDatos,
                _datos);
    }

    ~ObjetoPesado()
    {
      delete[] _datos;
    }
};

int main()
{
  clock_t start = clock();
  for( auto i=0; i<10000; i++ )
  {
    ObjetoPesado origen;

    // Llamada al constructor copia
    ObjetoPesado* copia = new ObjetoPesado(origen);
    delete copia;
  }
  std::cout << "copia: " << static_cast<double>(clock()-start) / CLOCKS_PER_SEC << std::endl;

  start = clock();
  for( auto i=0; i<10000; i++ )
  {
    ObjetoPesado origen;

    // Llamada al constructor move
    ObjetoPesado* copia = new ObjetoPesado(std::move(origen)); 
    delete copia;
  }
  std::cout << "move: " << static_cast<double>(clock()-start) / CLOCKS_PER_SEC << std::endl;
}

En mi caso, este programa, compilado sin optimizaciones para evitar que que se borren instrucciones, arroja el siguiente resultado:

copia: 22.929
move: 0.034

En este caso la nueva sintaxis es unas 670 veces más rápida que la versión tradicional, lo cual no está nada mal.

Vamos a analizar más de cerca el constructor move:

ObjetoPesado(ObjetoPesado&& origen)
  : _datos(nullptr)
{
  std::swap(_datos,origen._datos);
}

Lo primero que hace es inicializar su puntero interno a nullptr y después llama a swap(). Esta última función intercambia los valores de dos variables, de tal forma que _datos acaba apuntando a la memoria que hasta ese momento gestionaba origen._datos, dejando este último puntero apuntando a nullptr (aquí el por qué de la inicialización de _datos).

Como se aprecia, después de usar la sintaxis move el objeto original pierde su estado (al menos todo lo relacionado con su memoria dinámica), quedándo vacío. Esto se permite, repito, porque se asume que los Rvalue dejan de existir después de la expresión.

Del ejemplo propuesto también hay que destacar una función de nueva aparición en C++11 y cuya aparición se debe precisamente al nacimiento de toda esta operativa. Me refiero a move(), función que aparece en la línea 55. Esta función recibe un objeto y lo convierte en Rvalue. ¿Cómo obra esta magia? bueno, simplificando un poco podríamos decir que move() es un alias de static_cast. Una posible implementación de esta función:

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
  return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}

Simplificando un poco la explicación de esta función, tenemos dos partes:

  • remove_reference: un template que elimina el modificador '&' del tipo para evitar problemas. Si nos fijamos veremos que de dicho template únicamente le interesa su atributo type. Para más información sobre esta función recomiendo consultar la documentación online sobre este template.
  • static_cast, como vimos en la entrada de Rvalue y Lvalue, convierte un lValue en un rValue.

¿Cuándo se debe usar move()?

Ignorando el hecho de que, aunque su uso sea prácticamente gratis en este caso, static_cast() siempre va a tener un cierto coste para nuestro programa, conviene saber cuándo conviene y cuando no usar move(), al menos para mantener nuestro código lo más legible posible.

Desde C++11 los compiladores deberían realizar una serie de optimizaciones desconocidas hasta entonces. Una de ellas tiene que ver con los return. Cuando el compilador se topa con un return automáticamente va a convertir el objeto de retorno en un Rvalue, lo que va a permitir que se llame implícitamente al constructor o al operador de asignación move, según corresponda. Esto quiere decir que el siguiente uso de move() no es necesario:

ObjetoPesado func()
{
  ObjetoPesado obj;
  // ...
  return std::move(obj);
}

Lo correcto sería:

ObjetoPesado func()
{
  ObjetoPesado obj;
  // ...
  return obj;
}

De la misma forma, tampoco tiene sentido usar move() con el resultado devuelto por las funciones, ya que dicho resultado ya es un rValue:

// Incorrecto
ObjetoPesado obj = std::move(func());

// Correcto
ObjetoPesado obj = func();

El uso de move() cobra sentido cuando estamos trabajando con Lvalue. Un ejemplo:

ObjetoPesado obj;

if( something )
{
  ObjetoPesado temp;

  // ...

  if( something )
  {
    obj = std::move(temp);
  }
}

En este caso necesitamos trabajar con un objeto temporal y únicamente si se cumplen ciertas condiciones los datos de ese objeto temporal pasan a ser definitivos.

Otra situación en la que move() es de vital importancia la encontramos al trabajar con smart pointers, concretamente con unique_ptr, sobretodo si van integrados en contenedores:

std::vector<std::unique_ptr<POO>> items;

std::unique_ptr<POO> item = new POO;

// Error, unique_ptr tiene capado el consructor copia
items.push_back(item);

// Ok
items.push_back(std::move(item));

¿Es necesario implementar siempre el nuevo constructor y operador de asignación?

La respuesta es no. El compilador no nos va a obligar en ningún momento a implementar estas funciones.

Si el compilador necesita llamar al constructor move y no lo encuentra intentará llamar al constructor copia y únicamente si esta última llamada falla se provocará un error en tiempo de compilación. Esto es aplicable también para el operador de asignación.

Tambien hay que tener en cuenta que si nuestro objeto no hace uso de memoria dinámica (ni directa ni indirectamente) no vamos a obtener ningun beneficio implementando esta nueva sintaxis.


Rvalue y Lvalue

C++11 trajo numerosas novedades al lenguaje. Esta entrada la voy a dedicar a explicar la diferencia entre estos dos conceptos, pues son ampliamente utilizados desde la aparición de dicho estándar.

C++ entiende como Lvalue todo aquel elemento que puede ser modificado o que sobrevive a una instrucción mientras que etiquetará como Rvalue el resto de elementos.

¿Qué se supone que significa la frase anterior?

Antes de llegar a los ejempos que terminen de aclarar estos conceptos es importante indicar de dónde vienen estos nombres, ya que suponen toda una declaración de intenciones:
  • Rvalue viene de Right value, es decir, el valor de la derecha.
  • Lvalue significa Left value o valor de la izquierda.
Lo anterior viene a significar que normalmente los Lvalue se encontrarán en la parte izquierda de una expresión y los Rvalue a la derecha.

Veamoslo con ejempos. Imaginemos una sentencia típica de C++:

int x = 4;

En este caso x es un elemento que es modificable y que además va a sobrevivir a esa expresión. Después de esta instrucción podremos seguir usando x para nuestros propósitos. x será por tanto un Lvalue.

Por otro lado, el valor 4 es un literal y, por tanto, no puede ser modificado bajo ningún concepto. Además el literal no existirá una vez se haya ejecutado esa instrucción, pues no podemos referenciarlo de ninguna forma. El literal es un Rvalue.

¿Qué quiere decir que no podemos referenciarlo?

Significa literalmente eso, que su valor no puede ser asignado a una referencia:
// error: invalid initialization of non-const reference of type
// 'int&' from an rvalue of type 'int'
int &x = 2; 

Veamos otro ejemplo:

const int x = func();
int y = x; 
int z = x + y;
x = z;

En la primera línea tenemos un Lvalue que es la variable x y un Rvalue que es func(). Hay que destacar que tras esta instrucción, x pasará a ser Rvalue tras esta instrucción ya que está etiquetada como constante.

En la segunda línea tenemos a y, que es un Lvalue y a x que, como hemos comentado, es un Rvalue.

En la tercera línea tenemos dos Lvalue: y y z. La diferencia entre ambos es que y actuará de forma temporal como un Rvalue. Dado que un Rvalue es mucho más restrictivo que un Lvalue la conversión del segundo al primero esta permitida.

En la cuarta instrucción estamos intentando modificar el valor de un Rvalue, lo cual no está permitido. El compilador mostrará un error en esta línea.

Una forma rápida de identificar algunos Rvalue pasa por seguir la siguiente norma: "Si la expresión no tiene nombre, entonces es un Rvalue":

int x, y z;

// Error: x+1 es una expresión que no tiene nombre
x + 1 = z;

// Error: la expresión entre paréntesis tampoco tiene nombre
((x>3)? y : z) = 10;

// Error: 10 no tiene nombre, es un literal
10 = x + y;

// Error: la expresión static_cast hace que x se comporte como
// un Rvalue
static_cast<unsigned int>(x) = 5;

jueves, 21 de abril de 2016

La librería type_traits

Introducción

type_traits es una librería que se apareció en el estándar C++11, por lo que lleva poco tiempo entre nosotros. Desde mi punto de vista creo que es una librería que es fácil que pase desapercibida, puesto que no proporciona utilidades que sean indispensables a la hora de programar. Sin embargo, si se utiliza correctamente, se le puede llegar a sacar bastante partido.

La librería type_traits nos proporciona información en tiempo de compilación sobre los tipos de datos que estamos utilizando. Esto lo podemos utilizar tanto para acotar el uso que se les da a nuestro código como para programar optimizaciones de una forma muy sencilla.

A continuación tenemos un ejemplo bastante simple en el que hacemos uso de la librería type_traits:

#include <iostream>
#include <type_traits>

int main( )
{
  std::cout << "int tiene signo: "
            << std::is_signed< int >::value
            << std::endl;
  std::cout << "unsigned int tiene signo: "
            << std::is_signed< unsigned int >::value
            << std::endl;

  return 0;
}

Salida:

int tiene signo: 1
unsigned int tiene signo: 0

El ejemplo únicamente nos está indicando que el tipo int distingue positivos de negativos mientras que unsigned int no. A primera vista la información proporcionada por type_traits es trivial y puede hacer que nos cuestionemos la necesidad de usar la librería. Lo que sucede realmente es que esta librería está pensada para ser utilizada con templates y es ahí donde se le puede sacar todo el jugo.

Uso con templates

Si incluimos elementos de la librería type_traits en la declaración de nuestros templates conseguiremos una herramienta bastante potente que nos permitirá especializar nuestros templates con mucho menos esfuerzo del que teníamos que realizar hasta la fecha.

Imaginemos que necesitamos crear un template de función que nos indique si la variable pasada como argumento es una variable con signo o sin signo. Haciendo uso de las características tradicionales de los templates podríamos crear un programa como el siguiente:

#include <iostream>

template<typename T>
void
PrintValue( T value )
{
  std::cout << value << " is signed" << std::endl;
}

template< >
void
PrintValue( unsigned int value )
{
  std::cout << value << " is unsigned" << std::endl;
}

int main( )
{
  int intValue = 7;
  unsigned int uintValue = 200;

  PrintValue( intValue );
  PrintValue( uintValue );

  return 0;
}

Especializar un template de esta manera presenta numerosos inconvenientes:

  • Es necesario crear una especialización por cada tipo sin signo que necesitemos usar
  • La función dará un resultado incorrecto si la especializamos con un tipo no numérico
  • El código, debido a que tiende a ser demasiado largo y repetitivo, es difícil de mantener.

Para obtener un código funcional más o menos completo tendríamos que hacer tantas sobrecargas de PrintValue() como tipos sin signo necesitamos controlar. El código resultante será, por tanto, largo y complicado de mantener.

Ahora vamos a reescribir el código anterior haciendo uso de la librería type_traits. El código resultante es el siguiente:

#include <iostream>
#include <type_traits>

template<typename T>
typename std::enable_if<std::is_signed<T>::value, void>::type
PrintValue( T value )
{
  std::cout << value << " is signed" << std::endl;
}

template<typename T>
typename std::enable_if<std::is_unsigned<T>::value, void>::type
PrintValue( T value )
{
  std::cout << value << " is unsigned" << std::endl;
}

template<typename T>
typename std::enable_if<!std::is_integral<T>::value, void>::type
PrintValue( T value )
{
  std::cout << value << " is not a number!!!" << std::endl;
}

int main( )
{
  int intValue = 7;
  unsigned int uintValue = 200;

  PrintValue( intValue );
  PrintValue( uintValue );
  PrintValue( "test" );
}

Ahora con únicamente 3 versiones de la función hemos cubierto todo el espectro de posibilidades:

  • Números con signo
  • Números sin signo
  • Cualquier otra situación

Para entender el ejemplo anterior vamos a despiezar un poco una de las versiones del template:

template<typename T>
typename std::enable_if<std::is_signed<T>::value, void>::type
PrintValue( T value )
{
  std::cout << value << " is signed" << std::endl;
}

En este fragmento de código encontramos los siguientes elementos:

  • std::is_signed<T>::value: el miembro value de este template vale true cuando el tipo pasado equivale a un número con signo.
  • std::enable_if<..., void>::type: el tipo del miembro type de este template es idéntico al tipo pasado como segundo parámetro (void en este caso) sí y solo sí el primer parámetro es verdadero. Si el primer parámetro es falso, el miembro type de enable_if<> no estará definido.

Juntando estos dos elementos, resulta que el template que hemos puesto de ejemplo generará una especialización válida para aquellos tipos que sean números con signo y una función incorrecta (puesto que no habrá valor de retorno) en caso contrario. Espera, ¿una función incorrecta? ¿pero entonces no debería dar un error de compilación?

Pues bien, ejemplo funciona, no hay más que compilarlo y ejecutarlo para verificarlo. El motivo por el que funcione se debe a una regla de nueva aparición en C++11 conocida como SFINAE "Substitution Failure Is Not An Error". Esta regla viene a decir que si en la especialización de un template se produce un error de compilación, éste no se materializará si el compilador consigue encontrar otra versión del template que compile correctamente para el tipo dado.

Dicho en cristiano: Si un template tiene dos versiones y, para un tipo dado, la primera versión falla y la segunda no (o viceversa), el compilador no mostrará error alguno y compilará correctamente. En este caso, si intentamos especializar el template con un string, lo que sucede es que las dos primeras versiones del template darán error pero, al compilar correctamente la tercera versión, el compilador dará esa versión por buena y podrá continuar la compilación.

Revisando la lista de utilidades presentes en type_traits, descubrimos que podemos usarlas para adaptarnos a un sin fín de situaciones. Os expongo algunas que me han llamado la atención:

  • is_abstract: Verifica si el tipo se corresponde con una clase abstracta.
  • is_array: Permite identificar arrays crudos, es decir, en formato C.
  • is_pointer: Comprueba si el tipo se corresponde con un puntero.
  • is_const: Identifica tipos constantes.
  • is_copy_assignable: Nos indica si el operador de igualdad está disponible para el tipo dado.
  • ...

type_traits y static_assert

Otra característica nueva en C++11 es static_assert. Este nuevo elemento se presenta como un assert que se chequea en tiempo de compilación. Si el assert falla, provocará un fallo en la compilación y mostrará un mensaje de error elegido por nosotros mismos. La ventaja que supone realizar ciertos chequeos en tiempo de compilación es que podremos eliminar algunos if de nuestro código, con lo que mejoraremos el rendimiento de nuestros algoritmos.

Vamos con un ejemplo. Imaginemos que estamos creando un template para manejar un array de datos. El tamaño del array será fijo desde su creación y, por motivos de rendimiento, el tamaño del array no puede ser superior a 20:

template<typename T, int size>
class CustomArray
{
  static_assert(size < 20, "CustomArray length is too big");
};

int main( )
{
  CustomArray<char, 27> array;
  return 0;
}

Al probar el código del ejemplo, veremos que se produce un error en tiempo de compilación similar al siguiente:

..\main.cpp: In instantiation of 'class CustomArray<char, 27>':
..\main.cpp:9:27:   required from here
..\main.cpp:4:3: error: static assertion failed: CustomArray length is too big
   static_assert(size < 20, "CustomArray length is too big");

miércoles, 20 de abril de 2016

Presentación

Desde pequeño me ha gustado la programación. Mi relación con este mundo comienza a los 5 años, cuando los Reyes Magos me regalaron un AMTRAD 6128.

Ahora, unos cuantos años más tarde, por fín me he decidido a mantener un blog en el que poder documentar mis inquietudes sobre la programación. Mi idea inicial es publicar entradas en español por dos motivos: porque es mi lengua materna y porque considero que ya hay demasiados contenidos que únicamente se encuentran disponibles en inglés.

Espero que en este blog encontréis documentación de interés y que ello os anime a participar en esta aventura.

Bienvenidos a mi blog.

Un saludo.