Digamos que tengo lo siguiente class X
donde quiero devolver el acceso a un miembro interno:
class Z
{
// details
};
class X
{
std::vector<Z> vecZ;
public:
Z& Z(size_t index)
{
// massive amounts of code for validating index
Z& ret = vecZ[index];
// even more code for determining that the Z instance
// at index is *exactly* the right sort of Z (a process
// which involves calculating leap years in which
// religious holidays fall on Tuesdays for
// the next thousand years or so)
return ret;
}
const Z& Z(size_t index) const
{
// identical to non-const X::Z(), except printed in
// a lighter shade of gray since
// we're running low on toner by this point
}
};
Las dos funciones miembro X::Z()
y X::Z() const
tienen un código idéntico dentro de las llaves. Este es un código duplicado y puede causar problemas de mantenimiento para funciones largas con lógica compleja .
¿Hay alguna manera de evitar esta duplicación de código?
Respuestas:
Para obtener una explicación detallada, consulte el encabezado "Evitar duplicación
const
y función noconst
miembro", en la pág. 23, en el ítem 3 "Usarconst
siempre que sea posible", en Effective C ++ , 3d ed by Scott Meyers, ISBN-13: 9780321334879.Aquí está la solución de Meyers (simplificada):
Los dos lanzamientos y la llamada a la función pueden ser feos, pero es correcto. Meyers tiene una explicación detallada de por qué.
fuente
get()const
devuelve algo que se definió como un objeto const, entonces no debería haber una versión no constget()
en absoluto. En realidad, mi forma de pensar sobre esto ha cambiado con el tiempo: la solución de plantilla es la única forma de evitar la duplicación y obtener una corrección constante del compilador, por lo que personalmente ya no usaría unaconst_cast
para evitar la duplicación de código, elegiría entre poner el código duplicado en una plantilla de función o dejándolo duplicado.template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }
ytemplate<typename T> T& variable(const T& _) { return const_cast<T&>(_); }
. Entonces puedes hacer:return variable(constant(*this).get());
Sí, es posible evitar la duplicación de código. Debe usar la función miembro const para tener la lógica y hacer que la función miembro no const llame a la función miembro const y vuelva a emitir el valor de retorno a una referencia no const (o puntero si las funciones devuelven un puntero):
NOTA: Es importante que NO ponga la lógica en la función no const y que la función const llame a la función no const, ya que puede dar lugar a un comportamiento indefinido. La razón es que una instancia de clase constante se convierte como una instancia no constante. La función de miembro no constante puede modificar accidentalmente la clase, que según los estados estándar de C ++ dará como resultado un comportamiento indefinido.
fuente
C ++ 17 ha actualizado la mejor respuesta para esta pregunta:
Esto tiene las ventajas de que:
volatile
por accidente, perovolatile
es un calificador raro)Si desea ir a la ruta de deducción completa, eso se puede lograr con una función auxiliar
Ahora ni siquiera puedes equivocarte
volatile
, y el uso parecefuente
f()
devuelve enT
lugar deT&
.f()
regreseT
, no queremos tener dos sobrecargas,const
solo la versión es suficiente.shared_ptr
. Entonces, lo que realmente necesitaba era algo parecido a loas_mutable_ptr
que parece casi idéntico alas_mutable
anterior, excepto que toma y devuelve ayshared_ptr
usa enstd::const_pointer_cast
lugar deconst_cast
.T const*
entonces esto se uniría enT const* const&&
lugar de vincularse aT const* const&
(al menos en mi prueba lo hizo). Tuve que agregar una sobrecargaT const*
como tipo de argumento para los métodos que devuelven un puntero.Creo que la solución de Scott Meyers se puede mejorar en C ++ 11 mediante el uso de una función auxiliar tempate. Esto hace que la intención sea mucho más obvia y puede reutilizarse para muchos otros captadores.
Esta función auxiliar se puede usar de la siguiente manera.
El primer argumento es siempre el indicador de este. El segundo es el puntero a la función miembro a llamar. Después de eso, se puede pasar una cantidad arbitraria de argumentos adicionales para que se puedan enviar a la función. Esto necesita C ++ 11 debido a las plantillas variadic.
fuente
std::remove_bottom_const
que ir con nosotrosstd::remove_const
.const_cast
. Puede hacergetElement
una plantilla en sí misma y usar el rasgo del tipo dentro de losmpl::conditional
tipos que necesita, comoiterator
soconstiterator
s si es necesario. El verdadero problema es cómo generar una versión constante de un método cuando esta parte de la firma no se puede crear una plantilla.std::remove_const<int const&>
esint const &
(eliminar laconst
calificación de nivel superior), de ahí la gimnasia deNonConst<T>
esta respuesta. Putativostd::remove_bottom_const
podría eliminar laconst
calificación de nivel inferior y hacer precisamente lo queNonConst<T>
hace aquí:std::remove_bottom_const<int const&>::type
=>int&
.getElement
está sobrecargada. Entonces el puntero de función no puede resolverse sin dar los parámetros de la plantilla explícitamente. ¿Por qué?likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }
Complete: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83Un poco más detallado que Meyers, pero podría hacer esto:
El método privado tiene la propiedad indeseable de que devuelve un Z & no constante con una instancia constante, por lo que es privado. Los métodos privados pueden romper invariantes de la interfaz externa (en este caso, el invariante deseado es "un objeto constante no puede modificarse a través de referencias obtenidas a través de él a objetos que tiene-a").
Tenga en cuenta que los comentarios son parte del patrón: la interfaz de _getZ especifica que nunca es válido llamarlo (aparte de los accesores, obviamente): no hay ningún beneficio concebible de hacerlo de todos modos, porque es 1 carácter más para escribir y no lo hará resultar en un código más pequeño o más rápido. Llamar al método es equivalente a llamar a uno de los accesos con un const_cast, y tampoco querrás hacerlo. Si le preocupa que los errores sean obvios (y ese es un objetivo justo), llámelo const_cast_getZ en lugar de _getZ.
Por cierto, aprecio la solución de Meyers. No tengo ninguna objeción filosófica al respecto. Sin embargo, personalmente prefiero un poco de repetición controlada y un método privado que solo debe llamarse en ciertas circunstancias estrictamente controladas, en lugar de un método que parece ruido de línea. Elige tu veneno y quédate con él.
[Editar: Kevin ha señalado correctamente que _getZ podría querer llamar a un método adicional (digamos generateZ) que está especializado en constante de la misma manera que getZ. En este caso, _getZ vería un const Z & y tendría que const_cast antes de regresar. Eso sigue siendo seguro, ya que el accesorio repetitivo controla todo, pero no es muy obvio que sea seguro. Además, si haces eso y luego cambias generateZ para que siempre devuelva const, entonces también debes cambiar getZ para que siempre devuelva const, pero el compilador no te dirá que lo haces.
Este último punto sobre el compilador también es cierto para el patrón recomendado por Meyers, pero el primer punto sobre un const_cast no obvio no lo es. A fin de cuentas, creo que si _getZ necesita un const_cast para su valor de retorno, entonces este patrón pierde mucho de su valor sobre el de Meyers. Como también sufre desventajas en comparación con las de Meyers, creo que cambiaría a las suyas en esa situación. Refactorizar de uno a otro es fácil: no afecta a ningún otro código válido en la clase, ya que solo el código no válido y el repetitivo llaman a _getZ.]
fuente
something
en la_getZ()
función hay una variable de instancia? El compilador (o al menos algunos compiladores) se quejará de que dado que_getZ()
es const, cualquier variable de instancia a la que se haga referencia también es const. Entoncessomething
sería const (sería de tipoconst Z&
) y no se podría convertir aZ&
. En mi experiencia (ciertamente algo limitada), la mayoría de las vecessomething
es una variable de instancia en casos como este.const_cast
. Estaba destinado a ser un marcador de posición para el código requerido para obtener un retorno no constante del objeto const, no como un marcador de posición para lo que habría estado en el getter duplicado. Entonces "algo" no es solo una variable de instancia.Buena pregunta y buenas respuestas. Tengo otra solución, que no usa moldes:
Sin embargo, tiene la fealdad de requerir un miembro estático y la necesidad de usar la
instance
variable dentro de él.No consideré todas las posibles implicaciones (negativas) de esta solución. Por favor avíseme si alguno.
fuente
auto get(std::size_t i) -> auto(const), auto(&&)
. Por qué '&&'? Ahh, así que puedo decir:auto foo() -> auto(const), auto(&&) = delete;
this
palabras clave. Sugiero quetemplate< typename T > auto myfunction(T this, t args) -> decltype(ident)
esta palabra clave se reconozca como el argumento de instancia de objeto implícito y permita que el compilador reconozca que myfunction es un miembro oT
.T
se deducirá automáticamente en el sitio de la llamada, que siempre será el tipo de la clase, pero con calificación gratuita de cv.const_cast
) para permitir el retornoiterator
yconst_iterator
.static
se puede hacer en el ámbito del archivo en lugar del ámbito de la clase. :-)También podría resolver esto con plantillas. Esta solución es un poco fea (pero la fealdad está oculta en el archivo .cpp) pero proporciona una comprobación del compilador de la constancia y no hay duplicación de código.
archivo .h:
archivo .cpp:
La principal desventaja que puedo ver es que debido a que toda la implementación compleja del método se encuentra en una función global, es necesario que se apoyen de los miembros de X utilizando métodos públicos como GetVector () arriba (de los cuales siempre debe haber un versión const y no const) o puede hacer que esta función sea un amigo. Pero no me gustan los amigos.
[Editar: se eliminó la inclusión innecesaria de cstdio agregado durante la prueba.]
fuente
const_cast
(que podría usarse accidentalmente para enlazar algo que en realidad se supone que es compatible con algo que no lo es).¿Qué hay de mover la lógica a un método privado y solo hacer las cosas de "obtener la referencia y devolver" dentro de los captadores? En realidad, estaría bastante confundido acerca de los lanzamientos estáticos y constantes dentro de una función getter simple, ¡y lo consideraría feo, excepto por circunstancias extremadamente raras!
fuente
¿Es una trampa usar el preprocesador?
No es tan elegante como las plantillas o los moldes, pero hace que su intención ("estas dos funciones sean idénticas") sea bastante explícita.
fuente
Me sorprende que haya tantas respuestas diferentes, pero casi todas se basan en una gran plantilla de magia. Las plantillas son poderosas, pero a veces las macros las superan con concisión. La máxima versatilidad a menudo se logra combinando ambos.
Escribí una macro
FROM_CONST_OVERLOAD()
que se puede colocar en la función no const para invocar la función const.Ejemplo de uso:
Implementación simple y reutilizable:
Explicación:
Como se publica en muchas respuestas, el patrón típico para evitar la duplicación de código en una función miembro no constante es este:
Se puede evitar mucho de este repetitivo usando inferencia de tipos. Primero,
const_cast
se puede encapsular enWithoutConst()
, lo que infiere el tipo de su argumento y elimina el calificador const. En segundo lugar, se puede utilizar un enfoque similarWithConst()
para calificar constantemente elthis
puntero, lo que permite llamar al método const-overloaded.El resto es una macro simple que antepone la llamada con la calificación correcta
this->
y elimina const del resultado. Dado que la expresión utilizada en la macro es casi siempre una simple llamada a función con argumentos reenviados 1: 1, los inconvenientes de las macros, como la evaluación múltiple, no entran en acción. Los puntos suspensivos y__VA_ARGS__
también podrían usarse, pero no deberían ser necesarios porque las comas (como los separadores de argumentos) aparecen entre paréntesis.Este enfoque tiene varios beneficios:
FROM_CONST_OVERLOAD( )
const_iterator
,std::shared_ptr<const T>
, etc.). Para esto, simplemente sobrecargueWithoutConst()
los tipos correspondientes.Limitaciones: esta solución está optimizada para escenarios en los que la sobrecarga no constante está haciendo exactamente lo mismo que la sobrecarga constante, de modo que los argumentos pueden reenviarse 1: 1. Si su lógica es diferente y no está llamando a la versión constante a través de
this->Method(args)
, puede considerar otros enfoques.fuente
Para aquellos (como yo) que
Aquí hay otra toma:
Básicamente es una combinación de las respuestas de @Pait, @DavidStone y @ sh1 ( EDITAR : y una mejora de @cdhowie). Lo que agrega a la tabla es que se saldrá con una sola línea de código adicional que simplemente nombra la función (pero sin argumento o duplicación de tipo de retorno):
Nota: gcc no puede compilar esto antes de 8.1, clang-5 y hacia arriba, así como MSVC-19 están contentos (según el explorador del compilador ).
fuente
decltype()
usarse los s tambiénstd::forward
en los argumentos para asegurarse de que estamos usando el tipo de retorno correcto en el caso de que tengamos sobrecargasget()
que tomen diferentes tipos de referencias?NON_CONST
macro deduce el tipo de retorno incorrectamente yconst_cast
s al tipo incorrecto debido a la falta de reenvío en losdecltype(func(a...))
tipos. Reemplazarlos condecltype(func(std::forward<T>(a)...))
resuelve esto . (Solo hay un error de vinculador porque nuncaX::get
Aquí hay una versión C ++ 17 de la función auxiliar de plantilla estática, con una prueba SFINAE opcional.
Versión completa: https://godbolt.org/z/mMK4r3
fuente
Se me ocurrió una macro que genera pares de funciones const / non-const automáticamente.
Vea el final de la respuesta para la implementación.
El argumento de
MAYBE_CONST
está duplicado. En la primera copia,CV
se reemplaza por nada; y en la segunda copia se reemplaza conconst
.No hay límite en cuántas veces
CV
puede aparecer en el argumento macro.Sin embargo, hay un pequeño inconveniente. Si
CV
aparece dentro de paréntesis, este par de paréntesis debe tener el prefijoCV_IN
:Implementación:
Implementación anterior a C ++ 20 que no admite
CV_IN
:fuente
Por lo general, las funciones miembro para las que necesita versiones const y no const son getters y setters. La mayoría de las veces son de una sola línea, por lo que la duplicación de código no es un problema.
fuente
Hice esto para un amigo que justificó legítimamente el uso de
const_cast
... sin saberlo, probablemente habría hecho algo como esto (no muy elegante):fuente
Sugeriría una plantilla de función estática de ayuda privada, como esta:
fuente
Este artículo de DDJ muestra una forma de usar la especialización de plantilla que no requiere que uses const_cast. Sin embargo, para una función tan simple realmente no es necesaria.
boost :: any_cast (en un punto, ya no lo hace) usa un const_cast de la versión const que llama a la versión no const para evitar la duplicación. Sin embargo, no puede imponer semántica constante en la versión no constante, por lo que debe tener mucho cuidado con eso.
Al final, una duplicación de código está bien siempre que los dos fragmentos estén directamente uno encima del otro.
fuente
Para agregar a la solución proporcionada por jwfearn y kevin, aquí está la solución correspondiente cuando la función devuelve shared_ptr:
fuente
No encontré lo que estaba buscando, así que rodé un par de los míos ...
Este es un poco prolijo, pero tiene la ventaja de manejar muchos métodos sobrecargados del mismo nombre (y tipo de retorno) a la vez:
Si solo tiene un
const
método por nombre, pero todavía hay muchos métodos para duplicar, entonces puede preferir esto:Desafortunadamente, esto se rompe tan pronto como comienza a sobrecargar el nombre (la lista de argumentos del argumento del puntero de la función parece no estar resuelta en ese punto, por lo que no puede encontrar una coincidencia para el argumento de la función). Aunque también puedes crear una plantilla para salir de eso:
Pero los argumentos de referencia al
const
método no coinciden con los argumentos aparentemente por valor de la plantilla y se rompe.No estoy seguro de por qué.He aquí por qué .fuente