L-valores y R-valores

febrero 8, 2010

error: se requiere un l-valor como operando izquierdo de la asignación

Si has visto alguna vez este error quizás que hayas preguntado qué es exactamente un L-valor y para qué sirve. Es curioso que se puede programar durante muchos años en C++ (o en C, de donde se hereda este concepto) sin tener que preocuparse ni una sola vez por el significado de este mensaje de error. Basta con observar la línea fuente con detenimiento y el error resulta obvio:

    int x;
    &x = 1;


Sin embargo, en algunos contextos, como en la programación de templates, es importante distinguir los L-valores de los R-valores. Además el nuevo estándar de C++ utiliza esta diferencia para hacer algunas cosas interesantes…

Definición

En C++ una expresión es o bien un L-valor o bien un R-valor.
Y aquí surge la primera confusión: lo que es un L/R-valor no es el valor resultado de evaluar la expresión, sino la expresión en sí. Quizás sería más apropiado llamarlas L/R-expresiones, pero así es como se llaman.

El compilador utiliza esta clasificación para determinar que ciertas expresiones no pueden aparecer en ciertos contextos. Así algunos operadores, como el de asignación, exigen un L-valor a la izquierda y un R-valor a la derecha. De hecho, de ahí vienen los nombres: L de left, izquierda y R de right, derecha. Naturalmente, puede haber otras razones para no poder asignar a una expresión, como que sea de tipo constante, o de tipo array.

Hay quien prefiere interpretar la L como lugar, de manera que son L-valores aquellas expresiones que se refieren a objetos que ocupan un espacio de memoria concreto. En ese sentido son L-valores aquellas expresiones a las que se les puede aplicar el operador & (dirección de). Esta regla falla con los objetos temporales, que ocupan memoria, pero son R-valores.

Es importante observar que los L-valores siempre se pueden convertir a R-valores, de forma trivial y automática. Existe la intuición generalizada entre los estudiosos del lenguaje de que es esta conversión la que provoca el acceso a memoria (aunque no hay nada en el lenguaje que lo afirme expresamente). Por ejemplo:

    int a, b=1;
    a = b;

En la segunda línea tanto a como b son L-valores, pero la b está en un contexto en el que se exige un R-valor, por lo que sufre la conversión y se accede al valor contenido en la variable. La a, sin embargo no se convierte a R-valor, por lo que no se accede a su contenido.

Es interesante observar el caso de los arrays: el nombre de un array es un L-valor (aunque no es asignable), y en la conversión a R-valor cambia de tipo y se convierte (decae) en un puntero al primer elemento del array:

    int x[10];
    x[2] = 3;

La expresión x[2] equivale por definición a *(x+2). Aquí x en un L-valor de tipo ‘array de 10 enteros’, que se convierte a un R-valor de tipo ‘puntero a enteros’, luego se le suma 2, apuntando al tercer elemento del array, y luego el operador * des-referencia el puntero y obtiene un L-valor, al que se le asigna 3.

Cómo distinguirlos

Unas reglas sencillas para distinguir L-valores son:

  • Los nombres de variables o referencias
  • El resultado de des-referenciar un puntero con el operator * o []
  • El retorno de una función que devuelva una referencia
  • Un cast a tipo referencia

Mientras que son R-valores:

  • Literales, excepto cadenas de caracteres, que son arrays.
  • El resultado de cualquier operador aritmético o lógico
  • El retorno de una función que no devuelva una referencia
  • Cualquier objeto temporal, es decir que no tenga nombre propio
  • Un cast que no sea a tipo referencia

Usos

Los L-valores pueden usarse básicamente para tres cosas para las que no sirve un R-valor:

  • Para asignarles algo, con el operador =.
  • Para obtener su dirección, con el operador &.
  • Para inicializar una referencia.

Intentar cualquiera de estas cosas con un R-valor provocaría un error similar al copiado al principio de esta página.

Pero existe una excepción: se puede usar un R-valor para inicializar una referencia constante. Si el R-valor no es un objeto temporal se creará uno con una copia de la expresión y la referencia se vinculará a él. Además la vida del temporal se prolongará a la vida de la referencia.

Esto resulta útil para evitar una copia extra en el retorno de una función:

    std::string Nombre();
    //...
    const std::string &n = Nombre();

Naturalmente, cualquier compilador decente es capaz de evitar la copia extra de ese std::string, aun cuando no se declare la referencia, pero no está de más echarle una mano al optimizador.

Esta excepción es más útil, aunque menos obvia, en el paso de parámetros a funciones con conversión automática de tipo:

    void Funcion(const std::string &nombre);
    //...
    Funcion("foo.txt");

En la llamada a Funcion se pasa un literal de tipo array de caracteres, que, en la conversión a R-valor decae a puntero a caracteres. Al inicializar una referencia a std::string con un char* se crea un objeto temporal, un R-valor, al que se vincula la referencia. Naturalmente para que esto funcione la referencia debe ser constante.

Conclusión

Si después de leer esto has llegado a la conclusión de que la diferencia entre L-valores y R-valores no aporta nada que no se consiga con un poco de sentido común, probablemente tengas razón: para la programación del día a día estas sutilezas son innecesarias. ¡Es obvio que “2 = 3;” no va compilar nunca!

Pero el saber no ocupa lugar. Y ayuda a comprender algunos errores de compilación no tan obvios, sobre todo cuando hay templates implicados.

Además, el nuevo C++0x trae varias novedades importantes relativas a los R-valores…

Artículos relacionados:

  1. Tipos automáticos en C++0x con auto La palabra reservada auto es casi con toda seguridad la...
  2. Deducción de tipos en C++0x con decltype El otro día veíamos la deducción automática de tipos usando...
  3. Referencias a R-valor (2ª parte) En el post anterior describía cómo las referencias a R-valor...
  4. Referencias a R-valor (1ª parte) En un post anterior comentaba que en C++ no se...
  5. Referencias Hay quien considera que las referencias de C++ no son...

Deja un comentario