¿Cómo puedo eliminar la duplicación de código entre similares const y no const funciones miembro?

StackOverflow https://stackoverflow.com/questions/123758

Pregunta

Digamos que tengo la siguiente class X a 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 idéntico código dentro de las llaves.Este es el código duplicado y puede causar problemas de mantenimiento por largo funciones con lógica compleja.

Hay una manera de evitar esta duplicación de código?

¿Fue útil?

Solución 2

Sí, es posible evitar la duplicación de código.Usted necesidad de utilizar la const función miembro para tener la lógica y tiene el no-const miembro de la llamada a la función de la const función de miembro y re-emitir el valor de retorno para un no-const de referencia (o puntero si la función devuelve 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 usted haga NO poner la lógica de la no-const función y const-llamada a la función de la no-const de la función, ya que puede resultar en un comportamiento indefinido.La razón es que una constante de la clase de instancia se lanzó como un no-constante de instancia.La no-const de la función miembro puede accidentalmente modificar la clase, que el estándar de C++ estados resultar en un comportamiento indefinido.

Otros consejos

Para una explicación detallada, por favor consulte en el apartado "Evitar la Duplicación de const y Noconst La Función de miembro," en la p.23, en el Punto 3 De "Uso const siempre que sea posible," en Efectivos De C++, 3d ed por Scott Meyers, ISBN-13:9780321334879.

alt text

Aquí Meyers' solución (simplificado):

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

Los dos modelos y la llamada a la función puede ser feo, pero es correcto.Meyers tiene una explicación detallada de por qué.

Creo que Scott Meyers' solución puede ser mejorada en C++11 mediante el uso de un tempate función auxiliar.Esto hace que la intención mucho más evidente y puede ser reutilizado 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 puede ser utilizado 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 esta-puntero.El segundo es el puntero a la función miembro llamada.Después de que una cantidad arbitraria de adicionales se pueden pasar argumentos para que puedan ser remitidos a la función.Esto necesita de C++11 debido a la variadic plantillas.

Un poco más detallado Meyers, pero yo 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 el indeseable de la propiedad que devuelve un no-const Z& para una constante de instancia, que es la razón por la que es privado.Los métodos privados pueden romper los invariantes de la interfaz externa (en este caso el deseado invariante es "una constante objeto no puede ser modificado a través de las referencias obtenidas a través de ella a objetos-un").

Tenga en cuenta que los comentarios son parte del patrón - _getZ la interfaz especifica que nunca es válido para llamar (aparte de los descriptores de acceso, obviamente):no hay concebible beneficio a hacerlo de todos modos, porque es 1 más caracteres para escribir y no resultará en una menor o un código más rápido.La llamada al método es equivalente a llamar a uno de los descriptores de acceso con un const_cast, y usted no quiere hacer eso.Si usted está preocupado acerca de cometer errores obvios (y eso es un objetivo de la feria), a continuación, llamar const_cast_getZ en lugar de _getZ.

Por cierto, agradezco Meyers de la solución.Yo no tengo ninguna objeción filosófica a ella.Personalmente, sin embargo, yo prefiero un poco de control de la repetición, y un método privado que solo debe ser llamado en ciertos fuertemente controlado por las circunstancias, más de un método que parece el ruido de la línea.Escoja su veneno y se pega con él.

[Editar:Kevin ha señalado acertadamente que _getZ posible que desee llamar a un método más (dicen generateZ) que es const, especializadas en la misma forma getZ es.En este caso, _getZ iba a ver un const Z& y tienen que const_cast antes de regresar.Eso es todavía fuerte, ya que el repetitivo de los descriptores de políticas de todo, pero no es perfectamente obvio que es seguro.Además, si haces eso y luego cambiar generateZ para devolver siempre constante, entonces usted también necesita cambiar getZ para devolver siempre constante, pero el compilador no le digan que hacer.

Este último punto sobre el compilador también es cierto Meyers recomienda patrón, pero el primer punto sobre un no-obvio const_cast no lo es.Así que en definitiva creo que si _getZ resulta necesario un const_cast por su valor de retorno, entonces este patrón, pierde mucho de su valor a lo largo del Meyers.Ya que también sufre desventajas en comparación con los del Meyers, creo que me gustaría cambiar a su en esa situación.Refactorización de uno a otro es fácil, y no afectará a ningún otro código válido en la clase, ya que sólo el código no válido y repetitivo de las llamadas _getZ.]

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 un mínimo de código de arriba -- cabe en una sola línea
  • Es difícil equivocarse (sólo puede lanzar lejos volatile por accidente, pero volatile es una rara calificador)

Si quieres ir a la deducción completa de la ruta luego de que se puede lograr teniendo 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 se puede estropear volatile, y el uso que se parece a

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

Buena pregunta y agradable respuestas.Tengo otra solución, que no utiliza moldes:

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 a un miembro estático y la necesidad de utilizar el instance variable dentro de ella.

Yo no considerar todos los posibles (negativo) implicaciones de esta solución.Por favor, hágamelo saber si los hubiere.

También se podría resolver esto con plantillas.Esta solución es un poco feo (pero la fealdad ocultas en el .archivo cpp), pero sí proporciona el compilador de comprobación de constness, y no la duplicación de código.

.h archivo:

#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 veo es que debido a todo el complejo de la aplicación del método está en una función global, usted necesita conseguir el asimiento de los miembros de X utilizando métodos públicos como GetVector() superior (de la cual no siempre tiene que ser una constante y no const versión) o se puede hacer esta función a un amigo.Pero no me gustan los amigos.

[Editar:eliminado innecesario incluir de cstdio añadido durante la prueba.]

Cómo sobre el movimiento de la lógica en un método privado, y sólo haciendo el "obtener la referencia y retorno" cosas dentro de los getters?En realidad, creo que sería bastante confundido acerca de la estática y const proyecta en el interior de una simple función de captador, y me gustaría considerar que feo, excepto por circunstancias extremadamente raras!

Es la trampa para 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 plantillas o moldes, pero hace que tu intención ("estas dos funciones son idénticas") bastante explícito.

Normalmente, las funciones miembro para el cual se necesita const y no const versiones son getters y setters.La mayoría de las veces son de una sola línea para la duplicación de código no es un problema.

Esto lo hice para un amigo que justamente se justifica el uso de la const_cast...no saber que probablemente yo 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;
}

Me gustaría sugerir un auxiliar privado función estática de la plantilla, como este:

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);
    }
};

Para aquellos (como yo) que

  • uso c++17
  • desea agregar el menos cantidad de repetitivo/repetición y
  • no importa usar makros (mientras se espera para el meta-clases...),

aquí es otra toma:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T>                                                \
    auto func(T&&... a) -> typename NonConst<decltype(func(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

Básicamente es una mezcla de las respuestas de @Pait, @DavidStone y @sh1.Lo que se agrega a la tabla es que salir con sólo una línea de código adicional que simplemente los nombres de la función (pero no hay ningún argumento o el tipo de retorno de la duplicación):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Nota:gcc no se compila antes de 8.1, clang-5 y superiores, así como MSVC-19 son felices (de acuerdo a el compilador explorer).

Es sorprendente para mí que hay tantas respuestas diferentes, sin embargo, casi todos se basan en el pesado de la plantilla de la magia.Las plantillas son poderosos, pero a veces macros golpearon en la concisión.Máxima versatilidad a menudo se logra mediante la combinación de ambos.

Escribí un macro FROM_CONST_OVERLOAD() que puede ser colocado en la no-const de la función a invocar la const función.

Ejemplo de uso:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Simple y reutilizables de la aplicación:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Explicación:

Según lo publicado en muchas de las respuestas, el patrón típico de evitar la duplicación de código en un no-const de la función miembro es este:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Mucho de este repetitivo puede ser evitado utilizando el tipo de inferencia.En primer lugar, const_cast puede ser encapsulado en WithoutConst(), que deduce el tipo de su argumento y se quita la const-calificador.En segundo lugar, un enfoque similar puede ser utilizado en WithConst() a const calificar el this puntero, que permite llamar a la const-método sobrecargado.

El resto es una simple macro que se antepone a la llamada con el correctamente calificado this-> y quita const desde el resultado.Puesto que la expresión utilizada en el macro es casi siempre una simple llamada a la función con 1:1 reenviado argumentos, los inconvenientes de macros, tales como múltiples de evaluación no entran en juego.Los puntos suspensivos y __VA_ARGS__ también podría ser utilizado, pero no debería ser necesario debido a comas (como argumento separadores) se producen dentro de los paréntesis.

Este enfoque tiene varias ventajas:

  • Mínimo y natural de la sintaxis -- simplemente envuelva la llamada en FROM_CONST_OVERLOAD( )
  • Ningún miembro adicional de la función requerida
  • Compatible con C++98
  • Simple ejecución, la plantilla no metaprogramación y dependencias de cero
  • Extensible:otras const relaciones pueden ser añadidos (como la const_iterator, std::shared_ptr<const T>, etc.).Para esto, simplemente, la sobrecarga de WithoutConst() para los tipos correspondientes.

Limitaciones:esta solución está optimizado para los escenarios en los que la no-const de sobrecarga está haciendo exactamente la misma que la const sobrecarga, por lo que los argumentos pueden ser reenviados 1:1.Si su lógica es diferente, y no llamar a la const versión a través de this->Method(args), usted puede considerar otros enfoques.

Este DDJ artículo muestra una forma con la plantilla de especialización que no requieren el uso de const_cast.Para una función simple que en realidad no es necesario, aunque.

boost::any_cast (en un momento, no más), utiliza una const_cast de la const versión de llamar a la no-const versión de evitar la duplicación.Usted no puede imponer la const semántica sobre la no-const de la versión, así que tienes que ser muy cuidado con eso.

Al final algunos duplicación de código es bien, como los dos fragmentos son directamente en la parte superior de uno al otro.

Para agregar a la solución jwfearn y kevin siempre, 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;
};

No encontrar lo que estaba buscando, así que me di la vuelta un par de mis propias...

Este es un poco prolijo, pero tiene la ventaja de la manipulación de muchos métodos sobrecargados con el mismo nombre (y el tipo de retorno) todos 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 usted tiene sólo una const método por el nombre, pero todavía un montón de métodos para duplicar, entonces usted podría preferir este:

  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); }

Por desgracia, este se rompe tan pronto como usted comienza a sobrecargar el nombre (el puntero de la función argumento de la lista de argumentos parece ser resuelto en ese punto, así que no puede encontrar una coincidencia para el argumento de la función).Aunque puede plantilla de su manera de salir de esa, y también:

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

Pero la referencia a los argumentos de la const método no coinciden con las que al parecer por argumentos de valor para la plantilla y se rompe. No sé por qué.He aquí por qué.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top