c++ - after - La misma función con const y without-¿Cuándo y por qué?
const int (7)
T& f() { // some code ... }
const T& f() const { // some code ... }
He visto esto un par de veces ahora (en el libro de introducción que he estado estudiando hasta ahora). Sé que la primera const hace que el valor de retorno const, en otras palabras: no modificable. La segunda const permite que la función también pueda ser llamada para const declarar variables, creo.
Pero, ¿por qué tendría ambas funciones en una misma definición de clase? ¿Y cómo distingue el compilador entre estos? Creo que la segunda f () (con const) también se puede llamar para variables no constantes.
Pero, ¿por qué tendría ambas funciones en una misma definición de clase?
Tener ambos te permite:
- llame a la función en un objeto mutable y modifique el resultado si lo desea; y
- llamar a la función en un objeto
const
, y solo mirar el resultado.
Con solo el primero, no podrías llamarlo en un objeto const
. Con solo el segundo, no podría usarlo para modificar el objeto al que devuelve una referencia.
¿Y cómo distingue el compilador entre estos?
Selecciona la sobrecarga de const
cuando se llama a la función en un objeto const
(oa través de una referencia o puntero a const
). De lo contrario, elige la otra sobrecarga.
Creo que la segunda f () (con const) también se puede llamar para variables no constantes.
Si esa fuera la única sobrecarga, entonces podría. Con ambas sobrecargas, se seleccionaría la sobrecarga no const
su lugar.
Como se mencionó anteriormente, puede usar las versiones const
y non- const
de las funciones dependiendo de la constancia del objeto que invoca. El paradigma se usa muy a menudo con el operator[]
para matrices. Una forma de evitar la duplicación de código (tomada del libro Effective C ++ de Scott Meyers) es const_cast
la función const
en una sobrecarga no constante, como:
// returns the position of some internal char array in a class Foo
const char& Foo::operator[](std::size_t position) const
{
return arr[position]; // getter
}
// we now define the non-const in terms of the const version
char& Foo::operator[](std::size_t position)
{
return const_cast<char&>( // cast back to non-const
static_cast<const Foo&>(*this)[position] // calls const overload
); // getter/setter
}
El primero, sin const, permite al llamante modificar el objeto, que en general es un miembro de la clase cuyo método se está llamando.
El segundo, donde nuestra clase de host está en modo de solo lectura, también permite el acceso de solo lectura a sus miembros.
Por defecto, la versión no constante se invoca si está permitida bajo las reglas de constness.
Uno de los ejemplos más comunes de esto es con algún tipo de clase de colección / tipo de matriz.
class Array
{
private:
MyType members[MySize];
public:
MyType & operator[]( size_t index );
const MyType & operator[]( size_t index ) const;
};
Suponiendo que estén implementados y que podría ser una plantilla o son tipos y tamaños concretos. Estoy demostrando la sobrecarga const.
Ahora podemos tener a alguien usando la clase. Es posible que desee establecer un valor.
Array myArray;
myArray[ 3 ] = myObject;
O quizás solo lo estés leyendo:
const Array& myArrayRef = getArrayRef(); // gets it to read
const MyType & myValueRef = myArrayRef[ 3 ];
Así que ya ves, puedo usar la notación para establecer un valor y leer uno. Al igual que con el operator[]
, puede aplicar esta técnica a cualquier método.
Hay un muy buen ejemplo en el marco de Qt.
Echa un vistazo a la clase QImage .
Hay dos funciones públicas:
const uchar* scanLine (int i) const;
uchar* scanLine (int i);
El primero es para acceso de lectura solamente. El segundo es para el caso en el que desea modificar la línea de exploración.
¿Por qué es importante esta distinción? Porque Qt usa el intercambio de datos implícito . Esto significa que, QImage no realiza inmediatamente una copia profunda si haces algo como esto:
QImage i1, i2;
i1.load("image.bmp");
i2 = i1; // i1 and i2 share data
En su lugar, los datos se copian solo y solo si llama a una función que realmente modifica una de las dos imágenes, como la línea de escaneo no constante.
Le permite a ambos tener acceso a los datos de las instancias const de forma de solo lectura, mientras que aún puede modificar los datos de las instancias no const.
#include <iostream>
class test
{
public:
test() : data_(0) {}
int& f() { return data_; }
const int& f() const { return data_ }
private:
int data_;
};
int main(void)
{
const test rock;
test paper;
/* we can print both */
std::cout << rock.f() << std::endl;
std::cout << paper.f() << std::endl;
/* but we can modify only the non const one */
// rock.f() = 21;
paper.f() = 42;
}
Los calificadores después de los parens de llamada de función se aplican a this
parámetro oculto de funciones miembro:
Una función miembro void Foo::bar()
es algo así: void bar(Foo *this)
. Pero, ¿qué pasa si el objeto Foo es const
?
struct Foo {
void bar();
};
const Foo f{};
f.bar();
Bueno, dado que Foo::bar()
toma un Foo *this
parámetro, que no se permite que sea const
, el f.bar();
falla en compilar Así que necesitamos una forma de calificar el parámetro oculto, y la forma en que C ++ elige hacerlo es permitir que esos calificadores salgan de la función parens.
La forma en que el compilador distingue estas funciones es idéntica en todos los aspectos a la sobrecarga de funciones regulares, porque eso es exactamente lo que es, a pesar de la sintaxis extraña.
Además, const
no es el único calificador. También puede agregar calificadores volatile
, y en C ++ 11 también puede colocar calificadores de referencia lvalue y rvalue.
La razón por la que necesitamos dos copias casi idénticas de esta función es porque no hay una forma directa de extraer la única diferencia: los diferentes tipos de devolución. Si tenemos un objeto const, y ese objeto tiene un captador que devuelve una referencia a algo que contiene, esa referencia debe calificarse de la misma forma que el objeto general.
struct Foo {
int i;
int &get_i() const { return i; }
};
int main() {
const Foo f{};
f.get_i() = 10; // i should be const!
}
Lo anterior ni siquiera se compilará porque dentro de Foo::get_i() const
, i
es const, y no podemos devolverle una referencia que no sea constante. Pero si se permitiera, sería incorrecto porque no deberíamos poder modificar los miembros de un objeto const. Así que Foo::get_i() const
necesita devolver una referencia constante a i
.
int const &Foo::get_i() const { return i; }
Pero deberíamos poder modificar un miembro de un objeto no const,
int main() {
Foo f{};
f.get_i() = 10; // should be fine
}
así que no podemos tener solamente esta función. Necesitamos una función que devuelva una referencia no constante cuando el objeto Foo en sí no es constante. Entonces, sobrecargamos la función basada en la constancia del objeto:
struct Foo {
int i;
int const &get_i() const { return i; }
int &get_i() { return i; }
};
Si el cuerpo de la función es más complicado, hay una opción posible para evitar esa duplicación:
struct Foo {
int i;
int const &get_i() const { return i; }
int &get_i() { return const_cast<int &>(const_cast<Foo const *>(this)->get_i()); }
};
Es decir, la sobrecarga no constante delega su implementación a la sobrecarga const, utilizando const_cast para arreglar los tipos. Agregar const es siempre seguro. Eliminar const utilizando un const_cast solo es seguro cuando sabemos con certeza que el objeto original no es const. Lo sabemos en este caso, porque sabemos que agregamos la constante en primer lugar a un objeto no constante.
Pero, ¿por qué tendría ambas funciones en una misma definición de clase?
A veces desea proporcionar semántica diferente para la misma operación, dependiendo de si se invoca en el objeto const
o en el objeto non-const
. Tomemos un ejemplo de std::string
class: -
char& operator[](int index);
const char& operator[](int index) const;
En este caso, cuando el operator[]
invocado a través del objeto const
, no permitirá que el usuario cambie el contenido de la cadena.
const std::string str("Hello");
str[1] = ''A''; // You don''t want this for const.
Por otro lado, en el caso de una cadena no constante, permite al usuario cambiar el contenido de la cadena. Es por eso que una sobrecarga diferente.
¿Y cómo distingue el compilador entre estos?
El compilador comprueba si ese método se invoca en el objeto const
O el objeto non-const
y, a continuación, llama adecuadamente ese método.
const std::string str("Hello");
cout << str[1]; // Invokes `const` version.
std::string str("Hello");
cout << str[1]; // Invokes non-const version.