¿Cómo elimino la duplicación de código entre funciones similares const y non-const?

Digamos que tengo la siguiente class X donde quiero devolver el acceso a un miembro interno:

 class Z { // details }; class X { std::vector 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?

Para obtener una explicación detallada, consulte el título “Evitar duplicación en la función de miembro const y no const “, en la pág. 23, en el ítem 3 “Usar const siempre que sea posible”, en C ++ efectivo , ed 3d editado por Scott Meyers, ISBN-13: 9780321334879.

texto alternativo

Aquí está la solución de Meyers (simplificada):

 struct C { const char & get() const { return c; } char & get() { return const_cast(static_cast(*this).get()); } char c; }; 

Los dos lanzamientos y la llamada a la función pueden ser feos, pero es correcto. Meyers tiene una explicación completa de por qué.

Sí, es posible evitar la duplicación de código. Necesita usar la función de miembro constante para tener la lógica y hacer que la función miembro no const llame a la función const member y vuelva a convertir el valor devuelto a una referencia no const (o puntero si las funciones devuelven un puntero):

 class X { std::vector vecZ; public: const Z& Z(size_t index) const { // same really-really-really long access // and checking code as in OP // ... return vecZ[index]; } Z& Z(size_t index) { // One line. One ugly, ugly line - but just one line! return const_cast( static_cast(*this).Z(index) ); } #if 0 // A slightly less-ugly version Z& Z(size_t index) { // Two lines -- one cast. This is slightly less ugly but takes an extra line. const X& constMe = *this; return const_cast( constMe.Z(index) ); } #endif }; 

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 non-const, ya que puede dar como resultado 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, lo que los estados estándar de C ++ darán como resultado un comportamiento indefinido.

Creo que la solución de Scott Meyers se puede mejorar en C ++ 11 mediante el uso de una función de ayuda de Tempate. Esto hace que la intención sea mucho más obvia y se puede reutilizar para muchos otros getters.

 template  struct NonConst {typedef T type;}; template  struct NonConst {typedef T type;}; //by value template  struct NonConst {typedef T& type;}; //by reference template  struct NonConst {typedef T* type;}; //by pointer template  struct NonConst {typedef T&& type;}; //by rvalue-reference template typename NonConst::type likeConstVersion( TObj const* obj, TConstReturn (TObj::* memFun)(TArgs...) const, TArgs&&... args) { return const_cast::type>( (obj->*memFun)(std::forward(args)...)); } 

Esta función auxiliar se puede usar de la siguiente manera.

 struct T { int arr[100]; int const& getElement(size_t i) const{ return arr[i]; } int& getElement(size_t i) { return likeConstVersion(this, &T::getElement, i); } }; 

El primer argumento es siempre el este-puntero. El segundo es el puntero a la función miembro para llamar. Después de eso, se puede pasar una cantidad arbitraria de argumentos adicionales para que se puedan reenviar a la función. Esto necesita C ++ 11 debido a las plantillas variadas.

Un poco más detallado que Meyers, pero podría hacer esto:

 class X { private: // This method MUST NOT be called except from boilerplate accessors. Z &_getZ(size_t index) const { return something; } // boilerplate accessors public: Z &getZ(size_t index) { return _getZ(index); } const Z &getZ(size_t index) const { return _getZ(index); } }; 

El método privado tiene la propiedad indeseable de que devuelve una Z no const para una instancia const, por lo que es privada. Los métodos privados pueden romper invariantes de la interfaz externa (en este caso, la invariante deseada es “un objeto const no puede modificarse a través de las referencias obtenidas a través de él a los 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 accesadores, obviamente): no hay ningún beneficio concebible para hacerlo de todos modos, porque es un personaje más para escribir y no lo hará da como resultado un código más pequeño o más rápido. Llamar al método equivale a llamar a uno de los descriptores de acceso con un const_cast, y tampoco querrá hacer eso. Si te preocupa que los errores sean obvios (y ese es un objective justo), llámalo const_cast_getZ en lugar de _getZ.

Por cierto, aprecio la solución de Meyers. No tengo ninguna objeción filosófica al respecto. Personalmente, sin embargo, prefiero un poco de repetición controlada, y un método privado que solo debe invocarse en ciertas circunstancias estrictamente controladas, sobre un método que se parece al ruido de línea. Elija su veneno y quédese con él.

[Editar: Kevin ha señalado con razón que _getZ podría querer llamar a otro método (digamos generateZ) que está const-especializado de la misma manera que getZ es. En este caso, _getZ vería una const Z y tendrá que const_cast antes de regresar. Eso sigue siendo seguro, ya que el acceso repetitivo lo controla todo, pero no es muy obvio que sea seguro. Además, si haces eso y luego cambias generateZ para regresar siempre const, entonces también necesitas cambiar getZ para que siempre devuelva const, pero el comstackdor no te dirá que lo haces.

Este último punto sobre el comstackdor también es cierto para el patrón recomendado de Meyers, pero el primer punto acerca de un const_cast no obvio no lo es. Así que, en general, 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. Dado que también sufre desventajas en comparación con Meyers, creo que cambiaría a la suya en esa situación. La refactorización 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 inválido y las repetidas llamadas _getZ.]

Buena pregunta y buenas respuestas. Tengo otra solución, que no usa moldes:

 class X { private: std::vector v; template static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) { // massive amounts of code for validating index // the instance variable has to be used to access class members return instance.v[i]; } public: const Z& get(std::size_t i) const { return get(*this, i); } Z& get(std::size_t i) { return get(*this, i); } }; 

Sin embargo, tiene la fealdad de requerir un miembro estático y la necesidad de usar la variable de instance dentro de él.

No consideré todas las implicaciones (negativas) posibles de esta solución. Por favor, hágamelo saber si alguno.

C ++ 17 ha actualizado la mejor respuesta para esta pregunta:

 T const & f() const { return something_complicated(); } T & f() { return const_cast(std::as_const(*this).f()); } 

Esto tiene las ventajas de que:

  • Es obvio lo que está pasando
  • Tiene una sobrecarga de código mínima: se ajusta en una sola línea
  • Es difícil equivocarse (solo puede deshacerse de la volatile por accidente, pero la volatile es un calificador raro)

Si desea ir a la ruta de deducción completa, entonces eso se puede lograr teniendo una función de ayuda

 template constexpr T & as_mutable(T const & value) noexcept { return const_cast(value); } template void as_mutable(T const &&) = delete; 

Ahora no puedes arruinar la volatile , y el uso se ve como

 T & f() { return as_mutable(std::as_const(*this).f()); } 

También puedes resolver esto con plantillas. Esta solución es ligeramente fea (pero la fealdad está escondida en el archivo .cpp) pero proporciona una comprobación del comstackdor de constness y no duplicación de código.

.h archivo:

 #include  class Z { // details }; class X { std::vector vecZ; public: const std::vector& GetVector() const { return vecZ; } std::vector& GetVector() { return vecZ; } Z& GetZ( size_t index ); const Z& GetZ( size_t index ) const; }; 

archivo .cpp:

 #include "constnonconst.h" template< class ParentPtr, class Child > Child& GetZImpl( ParentPtr parent, size_t index ) { // ... massive amounts of code ... // Note you may only use methods of X here that are // available in both const and non-const varieties. Child& ret = parent->GetVector()[index]; // ... even more code ... return ret; } Z& X::GetZ( size_t index ) { return GetZImpl< X*, Z >( this, index ); } const Z& X::GetZ( size_t index ) const { return GetZImpl< const X*, const Z >( this, index ); } 

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, o necesita obtener los miembros de X utilizando métodos públicos como GetVector () anterior (de los cuales siempre debe haber un const y non-const version) o puede hacer de esta función un amigo. Pero no me gustan los amigos

[Editar: eliminado innecesario incluye de cstdio agregado durante la prueba.]

¿Qué hay de mover la lógica a un método privado, y solo hacer las cosas de “obtener la referencia y regresar” dentro de los getters? En realidad, estaría bastante confundido acerca de los moldes estáticos y const dentro de una función getter simple, y lo consideraría feo excepto en circunstancias extremadamente raras.

Hice esto por un amigo que legítimamente justificó el uso de const_cast … sin saberlo, probablemente habría hecho algo como esto (no muy elegante):

 #include  class MyClass { public: int getI() { std::cout < < "non-const getter" << std::endl; return privateGetI(*this); } const int getI() const { std::cout < < "const getter" << std::endl; return privateGetI(*this); } private: template  static T privateGetI(C c) { //do my stuff return c._i; } int _i; }; int main() { const MyClass myConstClass = MyClass(); myConstClass.getI(); MyClass myNonConstClass; myNonConstClass.getI(); return 0; } 

¿Es una trampa usar el preprocesador?

 struct A { #define GETTER_CORE_CODE \ /* line 1 of getter code */ \ /* line 2 of getter code */ \ /* .....etc............. */ \ /* line n of getter code */ // ^ NOTE: line continuation char '\' on all lines but the last B& get() { GETTER_CORE_CODE } const B& get() const { GETTER_CORE_CODE } #undef GETTER_CORE_CODE }; 

No es tan elegante como las plantillas o los moldes, pero hace que tu intención (“estas dos funciones sean idénticas”) sea bastante explícita.

Normalmente, las funciones de miembro para las que necesita las versiones const y non-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.

Para agregar a la solución jwfearn y kevin, esta es la solución correspondiente cuando la función devuelve shared_ptr:

 struct C { shared_ptr get() const { return c; } shared_ptr get() { return const_pointer_cast(static_cast(*this).get()); } shared_ptr c; }; 

Sugeriría una plantilla de función estática de ayuda privada, como esta:

 class X { std::vector vecZ; // ReturnType is explicitly 'Z&' or 'const Z&' // ThisType is deduced to be 'X' or 'const X' template  static ReturnType Z_impl(ThisType& self, size_t index) { // massive amounts of code for validating index ReturnType ret = self.vecZ[index]; // even more code for determining, blah, blah... return ret; } public: Z& Z(size_t index) { return Z_impl(*this, index); } const Z& Z(size_t index) const { return Z_impl(*this, index); } }; 

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), todo a la vez:

 struct C { int x[10]; int const* getp() const { return x; } int const* getp(int i) const { return &x[i]; } int const* getp(int* p) const { return &x[*p]; } int const& getr() const { return x[0]; } int const& getr(int i) const { return x[i]; } int const& getr(int* p) const { return x[*p]; } template auto* getp(Ts... args) { auto const* p = this; return const_cast(p->getp(args...)); } template auto& getr(Ts... args) { auto const* p = this; return const_cast(p->getr(args...)); } }; 

Si solo tiene un método const por nombre, pero hay muchos métodos para duplicar, puede preferir esto:

  template auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) { return const_cast((this->*f)(args...)); } int* getp_i(int i) { return pwrap(&C::getp_i, i); } int* getp_p(int* p) { return pwrap(&C::getp_p, p); } 

Desafortunadamente, esto se descompone tan pronto como comienza a sobrecargar el nombre (la lista de argumentos del argumento del puntero a la función parece no estar resuelta en ese momento, 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:

  template auto* getp(Ts... args) { return pwrap(&C::getp, args...); } 

Pero los argumentos de referencia para el método const no coinciden con los argumentos aparentemente con valores por debajo de la plantilla y se rompe. No estoy seguro por qué. He aquí por qué .

Este artículo de DDJ muestra una forma de utilizar la especialización de plantillas que no requiere el uso de const_cast. Para una función tan simple, sin embargo, realmente no es necesaria.

boost :: any_cast (en un punto, ya no existe) usa un const_cast de la versión de const llamando a la versión no const para evitar la duplicación. No se puede imponer la semántica const en la versión non-const, así que hay que tener mucho cuidado con eso.

Al final, la duplicación de código está bien siempre que los dos fragmentos estén directamente uno encima del otro.