¿Debo devolver objetos constantes?

78

En el Effective C++elemento 03, utilice constante siempre que sea posible.

class Bigint
{
  int _data[MAXLEN];
  //...
public:
  int& operator[](const int index) { return _data[index]; }
  const int operator[](const int index) const { return _data[index]; }
  //...
};

const int operator[]hace la diferencia de int& operator[].

Pero que pasa:

int foo() { }

y

const int foo() { }

Parece que son iguales.

Mi pregunta es, ¿por qué usamos en const int operator[](const int index) constlugar de int operator[](const int index) const?

abcdabcd987
fuente
4
Buena pregunta, aunque en este caso, ¿el método normalmente no devolvería una referencia en ambos casos?
Ken Wayne VanderLinde
Buena pregunta. Le sugiero que lo cambie a int operator[](int i)versus const int operator[](const int i).
Alexander Chertov
1
@KenWayneVanderLinde, no. El método llamado depende de si tenemos Bigint&o a const Bigint&.
Alexander Chertov
1
@AlexanderChertov no: pregunta sobre el tipo de retorno, no sobre el tipo de argumento, por lo que la pregunta debe centrarse solo en eso
stijn
¿Por qué? Personalmente, me pregunto por qué alguien tendría una const int idiscusión.
Alexander Chertov

Respuestas:

88

Se ignoran los calificadores de cv de nivel superior en tipos de devolución de tipos que no son de clase. Lo que significa que incluso si escribe:

int const foo();

El tipo de retorno es int. Si el tipo de retorno es una referencia, por supuesto, constya no es el nivel superior y la distinción entre:

int& operator[]( int index );

y

int const& operator[]( int index ) const;

es significante. (Tenga en cuenta también que en las declaraciones de funciones, como las anteriores, también se ignoran los calificadores cv de nivel superior).

La distinción también es relevante para los valores de retorno de tipo de clase: si regresa T const, entonces la persona que llama no puede llamar a funciones no constantes en el valor devuelto, por ejemplo:

class Test
{
public:
    void f();
    void g() const;
};

Test ff();
Test const gg();

ff().f();             //  legal
ff().g();             //  legal
gg().f();             //  **illegal**
gg().g();             //  legal
James Kanze
fuente
6
ff y gg se declaran como funciones que devuelven Test. Si es así, las invocaciones deben ser ff (). F () etc ... Si ff es una instancia de la clase Test, los paréntesis deben eliminarse de la declaración (+1 sin embargo :)
user396672
@ user396672 (y BJ ...) Sí. Olvidé el ()después ffy gg. Los agregaré. Gracias por la corrección.
James Kanze
+1 para todo lo que sigue a "La distinción también es relevante para los valores de retorno del tipo de clase". Esa aplicación de constes útil, sin embargo, muchas personas parecen pasarla por alto o insistir en que devolver un constvalor no tiene ninguna utilidad.
underscore_d
4
−1 “Se ignoran los calificadores cv de nivel superior en tipos de retorno de tipos que no son de clase” es incorrecto. Siguen siendo parte del tipo de función. Sin embargo, la calificación cv de un prvalue de tipo que no es de clase se ignora, por lo que una llamada de una función que regresa int const, por ejemplo, es equivalente a una llamada de esa función modificada para regresar int. Standardese en C ++ 11 §3.10 / 4, “Los valores de clase pueden tener tipos calificados por cv; los prvalues ​​que no son de clase siempre tienen tipos cv-no calificados ”.
Saludos y hth. - Alf
38

Debe distinguir claramente entre el uso constante que se aplica a los valores de retorno, los parámetros y la función en sí.

Valores devueltos

  • Si la función regresa por valor , la constante no importa, ya que se devuelve la copia del objeto. Sin embargo, será importante en C ++ 11 con la semántica de movimientos involucrada.
  • Tampoco importa para los tipos básicos, ya que siempre se copian.

Considere const std::string SomeMethod() const. No permitirá (std::string&&)que se use la función, ya que espera rvalue no constante. En otras palabras, la cadena devuelta siempre se copiará.

  • Si la función regresa por referencia , constprotege el objeto devuelto de ser modificado.

Parámetros

  • Si pasa un parámetro por valor , la constante evita la modificación de un valor dado por función en función . Los datos originales del parámetro no se pueden modificar de todos modos, ya que solo tiene una copia.
  • Tenga en cuenta que dado que la copia siempre se crea, la const solo tiene significado para el cuerpo de la función , por lo tanto, se verifica solo en la definición de la función, no en la declaración (interfaz).
  • Si pasa un parámetro por referencia , se aplica la misma regla que en los valores devueltos

Función en sí

  • Si la función tiene constal final, solo puede ejecutar otras constfunciones y no puede modificar o permitir la modificación de los datos de la clase. Por lo tanto, si regresa por referencia, la referencia devuelta debe ser constante. Solo las funciones const se pueden llamar en un objeto o una referencia a un objeto que es const en sí mismo. También se pueden cambiar los campos mutables.
  • El comportamiento creado por el compilador cambia de thisreferencia T const*. La función siempre puede const_cast this, pero, por supuesto, esto no se debe hacer y se considera inseguro.
  • Por supuesto, es sensato usar este especificador solo en métodos de clase; Las funciones globales con const al final generarán un error de compilación.

Conclusión

Si su método no modifica y nunca modificará las variables de clase, márquelo como constante y asegúrese de cumplir con todos los criterios necesarios. Permitirá que se escriba un código más limpio, manteniéndolo const-correcto . Sin embargo, poner en consttodas partes sin pensar en ello ciertamente no es el camino a seguir.

Bartek Banachewicz
fuente
4
Dos correcciones: primero, para la devolución por valor, el nivel superior constse ignora para los tipos que no son de clase . Importa para los tipos de clases. Y para los parámetros pasados ​​por valor, el nivel superior constse ignora en las declaraciones de funciones. Solo tiene significado en la definición.
James Kanze
Incorporaré este comentario en mi respuesta, si no le importa. Creo que entonces quedará mucho más claro.
Bartek Banachewicz
En cuanto consta una función: modifica el tipo de this, que es T const*, en lugar de T*. Todos los demás efectos se derivan de esto. (Y no evita todas las modificaciones del objeto: los mutabledatos aún se pueden cambiar, y la función puede desechar legalmente const y cambiar lo que desee).
James Kanze
"Si la función devuelve por referencia, protege el objeto devuelto para que no se modifique" falso
NoSenseEtAl
Ay, quise decir ", [const] protege el objeto" ... Lo arreglaré para que sea más legible.
Bartek Banachewicz
30

Hay poco valor en agregar constcalificaciones a valores r que no son de referencia / no punteros, y no tiene sentido agregarlos a los valores integrados.

En el caso de los tipos definidos por el usuario , una constcalificación evitará que las personas que llaman invoquen una constfunción no miembro en el objeto devuelto. Por ejemplo, dado

const std::string foo();
      std::string bar();

luego

foo().resize(42);

estaría prohibido, mientras

bar().resize(4711);

estaría permitido.

Para integradas como int, esto no tiene ningún sentido, porque tales valores r no se pueden modificar de todos modos.

(Sin embargo, recuerdo que Effective C ++ discutió cómo hacer el tipo de retorno de operator=()una constreferencia, y esto es algo a considerar).


Editar:

Parece que Scott dio ese consejo . Si es así, debido a las razones dadas anteriormente, lo encuentro cuestionable incluso para C ++ 98 y C ++ 03 . Para C ++ 11, lo considero claramente incorrecto , como parece haber descubierto el propio Scott. En las erratas para C ++ efectivo, 3ª ed. , escribe (o cita a otros que se quejaron):

El texto implica que todos los retornos por valor deben ser constantes, pero los casos en los que los retornos por valor no constantes tienen un buen diseño no son difíciles de encontrar, por ejemplo, tipos de retorno de std :: vector donde los llamadores usarán swap con un vector vacío para "tomar" el contenido del valor de retorno sin copiarlo.

Y después:

Declarar los valores de retorno de la función por valor const evitará que estén vinculados a referencias de rvalue en C ++ 0x. Debido a que las referencias de rvalue están diseñadas para ayudar a mejorar la eficiencia del código C ++, es importante tener en cuenta la interacción de los valores de retorno constantes y la inicialización de las referencias de rvalue al especificar firmas de funciones.

sbi
fuente
1
Me encanta la mención de erratas con el consejo de Scott. Realmente ayuda a arrojar algo de luz sobre lo que de otro modo podría ser confuso para la gente nueva a Efectiva C ++
sehe
2
@sehe: Le envié a Scott un correo preguntándole sobre este tema, y ​​su respuesta fue que esos comentarios de erratas reflejan su pensamiento actual sobre el tema.
sbi
17

Es posible que pierda el sentido del consejo de Meyers. La diferencia esencial está en el constmodificador del método.

Este es un método no constante (observe no constal final) lo que significa que puede modificar el estado de la clase.

int& operator[](const int index)

Este es un método constante (aviso constal final)

const int operator[](const int index) const

¿Qué pasa con los tipos de parámetro y el valor de retorno? Hay una pequeña diferencia entre inty const int, pero no es relevante para el punto del consejo. A lo que debe prestar atención es que la sobrecarga no constante devuelve, lo int&que significa que puede asignarle, por ejemplo num[i]=0, y la sobrecarga constante devuelve un valor no modificable (sin importar si el tipo de retorno es into const int).

En mi opinión personal, si un objeto se pasa por valor , el constmodificador es superfluo. Esta sintaxis es más corta y logra el mismo

int& operator[](int index);
int operator[](int index) const;
Andrey
fuente
5
En realidad, la cuestión es devolver un constvalor.
juanchopanza
@juanchopanza: Dudo que OP quisiera decir exactamente eso. Compare su fooejemplo con el operator[]ejemplo. Podría perder el punto de que el constmodificador para el método marca la diferencia, no el tipo de retorno.
Andrey
13

La razón principal para devolver valores como const es que no puede decir algo como foo() = 5;. En realidad, esto no es un problema con los tipos primitivos, ya que no se pueden asignar valores r de tipos primitivos, pero es un problema con los tipos definidos por el usuario (como (a + b) = c;, con una sobrecarga operator+).

Siempre he encontrado la justificación para eso bastante endeble. No se puede detener a alguien que tiene la intención de escribir un código incómodo, y este tipo particular de coerción no tiene ningún beneficio real en mi opinión.

Con C ++ 11, en realidad hay mucho daño que este idioma está haciendo: devolver valores como const evita las optimizaciones de movimiento y, por lo tanto, debe evitarse siempre que sea posible. Básicamente, ahora consideraría esto como un anti-patrón.

Aquí hay un artículo relacionado tangencialmente sobre C ++ 11.

Kerrek SB
fuente
¿qué hay de decir que es importante modificar el valor de retorno? Como MyClass.isInitialized (). Si la persona que llama cambia el valor de retorno, iría WTF en la revisión del código.
NoSenseEtAl
@NoSenseEtAl: ¿Quién soy yo para saber qué es inútil? Por lo que sabemos, el operador de asignación podría modificar un estado global u ordenar pizza ...
Kerrek SB
Agregué ejemplo. A mi comentario pasado. Y usted es el diseñador de la clase, por lo que comprende el operador = de sus tipos de devolución: D. Para que sepa si no tiene sentido que la persona que llama cambie el resultado de sus métodos.
NoSenseEtAl
9

Cuando se escribió ese libro, el consejo fue de poca utilidad, pero sirvió para evitar que el usuario escribiera, por ejemplo, foo() = 42;y esperara que cambiara algo persistente.

En el caso de operator[], esto puede ser un poco confuso si no proporciona también una no constsobrecarga que devuelva una no constreferencia, aunque quizás pueda evitar esa confusión devolviendo una constreferencia o un objeto proxy en lugar de un valor.

En estos días, es un mal consejo, ya que le impide vincular el resultado a una constreferencia (no ) de valor.

(Como se señaló en los comentarios, la pregunta es discutible cuando se devuelve un tipo primitivo como int, ya que el lenguaje le impide asignar a uno rvaluede ese tipo; estoy hablando del caso más general que incluye la devolución de tipos definidos por el usuario. )

Mike Seymour
fuente
10
Su ejemplo volvió int. foo() = 42;es ilegal, independientemente de si el tipo de retorno se declara into int const. No creo que Scott defendiera el uso de consten lugares como tipos de retorno y parámetros en declaraciones de funciones, donde el compilador lo ignora.
James Kanze
@JamesKanze: Gracias por señalar eso; Debería haber dejado claro que estaba hablando de manera más general.
Mike Seymour
2

Para tipos primitivos (como int), la consistencia del resultado no importa. Para las clases, podría cambiar el comportamiento. Por ejemplo, es posible que no pueda llamar a un método que no sea constante en el resultado de su función:

class Bigint {
    const C foo() const { ... }
     ...
}

Bigint b;
b.foo().bar();

Lo anterior está prohibido si bar()es una función de miembro constante de C. En general, elija lo que tenga sentido.

Grzegorz Herman
fuente
0

Mira eso:

const int operator[](const int index) const

el constfinal de la declaración. Describe que este método se puede llamar en constantes.

En la otra mano cuando escribes solo

int& operator[](const int index)

solo se puede llamar en instancias no constantes y también proporciona:

big_int[0] = 10;

sintaxis.

Hauleth
fuente
0

Una de sus sobrecargas es devolver una referencia a un elemento en la matriz, que luego puede cambiar.

int& operator[](const int index) { return _data[index]; }

La otra sobrecarga devuelve un valor para que lo use.

const int operator[](const int index) const { return _data[index]; }

Dado que llama a cada una de estas sobrecargas de la misma manera, una nunca cambiará el valor cuando se use.

int foo = myBigInt[1]; // Doesn't change values inside the object.
myBigInt[1] = 2; // Assigns a new value at index `
Esteta
fuente
0

En el ejemplo del operador de indexación de matrices ( operator[]), hace una diferencia.

Con

int& operator[](const int index) { /* ... */ }

puede usar la indexación para cambiar directamente la entrada en la matriz, por ejemplo, usándola así:

mybigint[3] = 5;

El segundo, el const int operator[](const int)operador se utiliza para obtener el valor únicamente.

Sin embargo, como valor de retorno de funciones, para tipos simples como, por ejemplo int, no importa. Lo que importa es si está devolviendo tipos más complejos, digamos a std::vector, y no desea que la persona que llama a la función modifique el vector.

Algún tipo programador
fuente
0

El consttipo de devolución no es tan importante aquí. Desde temporales int no son modificables, no hay ninguna diferencia observable en el uso intvs const int. Vería una diferencia si utilizara un objeto más complejo que pudiera modificarse.

Ken Wayne Vander como Linde
fuente
0

Hay algunas buenas respuestas que giran en torno al tecnicismo de las dos versiones. Para un valor primitivo, no hace ninguna diferencia.

Sin embargo , siempre he considerado constestar ahí para el programador más que para el compilador. Cuando escribe const, está diciendo explícitamente "esto no debería cambiar". Seamos sinceros,const generalmente se puede eludir de todos modos, ¿verdad?

Cuando devuelve un const, le está diciendo al programador que usa esa función que el valor no debería cambiar. Y si lo está cambiando, probablemente esté haciendo algo mal, porque no debería tener que hacerlo.

EDITAR: También creo que "usar constsiempre que sea posible" es un mal consejo. Debería usarlo donde tenga sentido.

Luchian Grigore
fuente