programas metodos ejemplos completo comandos codigo clases basico c++ class const code-duplication c++-faq

metodos - manual de c++ basico



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

¿Es engañoso 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 sofisticado como las plantillas o los modelos, pero hace que su intención ("estas dos funciones sean idénticas") sea bastante explícita.

Digamos que tengo la siguiente class X en la que 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 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?


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


Buena pregunta y buenas respuestas. Tengo otra solución, que no utiliza fundidos:

class X { private: std::vector<Z> v; template<typename InstanceType> 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 posibles implicaciones (negativas) 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<T &>(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: cabe en una sola línea
  • Es difícil equivocarse (solo puede volverse volatile por accidente, pero volatile es un calificador raro)

Si desea ir a la ruta de deducción completa, entonces esto se puede lograr al tener una función auxiliar

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

Ahora ni siquiera puedes estropear la volatile , y el uso parece

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


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

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

Esta función auxiliar se puede utilizar 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 puntero 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 puedan reenviarse a la función. Esto necesita C ++ 11 debido a las plantillas variadic.


Hice esto para un amigo que justificaba con razón el uso de const_cast ... sin saberlo, probablemente habría hecho algo como esto (no muy elegante):

#include <iostream> class MyClass { public: int getI() { std::cout << "non-const getter" << std::endl; return privateGetI<MyClass, int>(*this); } const int getI() const { std::cout << "const getter" << std::endl; return privateGetI<const MyClass, const int>(*this); } private: template <class C, typename T> 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; }


No encontré lo que estaba buscando, así que hice un par de mis propios ...

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:

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<typename... Ts> auto* getp(Ts... args) { auto const* p = this; return const_cast<int*>(p->getp(args...)); } template<typename... Ts> auto& getr(Ts... args) { auto const* p = this; return const_cast<int&>(p->getr(args...)); } };

Si solo tiene un método const por nombre, pero todavía hay muchos métodos para duplicar, puede que prefiera esto:

template<typename T, typename... Ts> auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) { return const_cast<T*>((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 rompe tan pronto como comienza a sobrecargar el nombre (la lista de argumentos del argumento de puntero a función parece no estar resuelta en ese punto, por lo que no puede encontrar una coincidencia para el argumento de función). Aunque también puedes salir de eso con una plantilla:

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

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


Para agregar a la solución jwfearn y kevin provista, aquí está la solución correspondiente cuando la función devuelve shared_ptr:

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


Para obtener una explicación detallada, consulte el encabezado "Evitar la duplicación en funciones miembro const y no const ," en la pág. 23, en el Artículo 3 "Usar const siempre que sea posible", en Efectivo C ++ , editado en 3D por Scott Meyers, ISBN-13: 9780321334879.

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

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

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


Por lo general, las funciones 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.


Sí, es posible evitar la duplicación de código. Debe usar la función miembro constante para que la lógica y la función miembro no constante llamar a la función miembro constante y volver a emitir el valor de retorno a una referencia no constante (o puntero si las funciones devuelven un puntero):

class X { std::vector<Z> 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<Z&>( static_cast<const X&>(*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<Z&>( constMe.Z(index) ); } #endif };

NOTA: Es importante que NO coloque la lógica en la función no constante y que la función constante llame a la función no constante, 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 miembro no constante puede modificar accidentalmente la clase, que los estados estándar de C ++ darán como resultado un comportamiento indefinido.


También podrías 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:

#include <vector> class Z { // details }; class X { std::vector<Z> vecZ; public: const std::vector<Z>& GetVector() const { return vecZ; } std::vector<Z>& 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 realiza en una función global, es necesario que adquiera los miembros de X utilizando métodos públicos como GetVector () más arriba (de los cuales siempre es necesario que haya un versión const y non-const) o puede hacer que esta función sea un amigo. Pero no me gustan los amigos.

[Edición: se eliminó la inclusión innecesaria de cstdio agregado durante la prueba.]


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 constante para una instancia de const, por lo que es privado. Los métodos privados pueden interrumpir invariantes de la interfaz externa (en este caso, el invariante deseado es "un objeto const no puede modificarse mediante referencias obtenidas a través de 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 que acceden, obviamente): de todos modos, no hay un beneficio imaginable, porque es 1 carácter más que escribir y no lo hará. resultado en código más pequeño o más rápido. Llamar al método es equivalente a llamar a uno de los accesores con un const_cast, y tampoco querrá hacer eso. Si le preocupa que los errores sean obvios (y ese es un objetivo justo), entonces llámelo const_cast_getZ en lugar de _getZ.

Por cierto, aprecio la solución de Meyers. No tengo ninguna objeción filosófica a ello. Personalmente, sin embargo, prefiero un poco de repetición controlada y un método privado que solo debe llamarse en ciertas circunstancias controladas, en lugar de un ruido de línea. Elige tu veneno y quédate con él.

[Editar: Kevin ha señalado con razón que _getZ podría querer llamar a un método adicional (por ejemplo, generatorZ) que está muy especializado de la misma manera que getZ. En este caso, _getZ vería una constante Z y tendría que const_cast antes de regresar. Eso sigue siendo seguro, ya que el accesorio de caldera cubre todo, pero no es muy obvio que sea seguro. Además, si haces eso y luego cambias genero para devolver siempre const, entonces también debes cambiar getZ para devolver siempre const, pero el compilador no te dirá que lo haces.

Este último punto sobre el compilador también es válido para el patrón recomendado de Meyers, pero el primer punto sobre un const_cast no obvio no lo es. Entonces, en general, creo que si _getZ resulta necesitar un const_cast para su valor de retorno, entonces este patrón pierde mucho de su valor sobre el de Meyers. Como también tiene 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 de la clase, ya que solo el código no válido y el texto de la caldera llama a _getZ.]


Yo sugeriría una plantilla de función estática auxiliar privada, como esta:

class X { std::vector<Z> vecZ; // ReturnType is explicitly ''Z&'' or ''const Z&'' // ThisType is deduced to be ''X'' or ''const X'' template <typename ReturnType, typename ThisType> 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<Z&>(*this, index); } const Z& Z(size_t index) const { return Z_impl<const Z&>(*this, index); } };


Este artículo de DDJ muestra una manera de utilizar la especialización de plantillas que no requiere que use const_cast. Sin embargo, para una función tan simple realmente no es necesaria.

boost :: any_cast (en un momento dado, ya no lo hace) utiliza un const_cast de la versión const que llama a la versión no constante para evitar la duplicación. No se puede imponer la semántica constante en la versión no constante, así que debes tener mucho cuidado con eso.

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