Estrategias Const C ++ DRY

14

Para evitar la duplicación no trivial relacionada con const de C ++, ¿hay casos en que const_cast funcionaría pero una función const privada que devuelve no const no funcionaría?

En el artículo 3 de C ++ efectivo de Scott Meyers , sugiere que un const_cast combinado con un reparto estático puede ser una forma efectiva y segura de evitar el código duplicado, por ejemplo

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

Meyers continúa explicando que hacer que la función const llame a la función no const es peligroso.

El siguiente código es un contraejemplo que muestra:

  • Contrariamente a la sugerencia de Meyers, a veces el const_cast combinado con un reparto estático es peligroso.
  • a veces hacer que la función const llame al no const es menos peligroso
  • a veces, las dos formas de usar const_cast ocultan errores de compilación potencialmente útiles
  • evitar una const_cast y tener un miembro privado const adicional que devuelva una no const es otra opción

¿Alguna de las estrategias const_cast para evitar la duplicación de código se considera una buena práctica? ¿Preferirías la estrategia de método privado en su lugar? ¿Hay casos en los que const_cast funcionaría pero un método privado no? ¿Hay otras opciones (además de la duplicación)?

Mi preocupación con las estrategias const_cast es que incluso si el código es correcto cuando se escribe, más adelante durante el mantenimiento, el código podría volverse incorrecto y const_cast ocultaría un error útil del compilador. Parece que una función privada común es generalmente más segura.

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};
JDiMatteo
fuente
Un captador que solo devuelve un miembro es más corto que uno que lanza y llama a la otra versión de sí mismo. El truco está destinado a funciones más complicadas donde la ganancia de deduplicación supera los riesgos de lanzamiento.
Sebastian Redl
@SebastianRedl Estoy de acuerdo en que la duplicación sería mejor si solo regresara miembro. Imagine que es más complicado, por ejemplo, en lugar de devolver mConstLongLived, podríamos llamar a una función en mConstLongLived que devuelve una referencia constante que luego se utiliza para llamar a otra función que devuelve una referencia constante que no es nuestra y que solo tenemos acceso a una versión constante de. Espero que el punto sea claro de que const_cast puede eliminar const de algo a lo que de otro modo no tendríamos acceso sin const.
JDiMatteo
44
Todo esto parece un poco ridículo con ejemplos simples, pero la duplicación relacionada con const aparece en código real, los errores del compilador de const son útiles en la práctica (a menudo para detectar errores estúpidos), y me sorprende que la solución propuesta "C ++ efectiva" sea extraña y un par de lanzamientos aparentemente propensos a errores. Un miembro de const privado que devuelve un no const parece claramente superior a un elenco doble, y quiero saber si hay algo que me falta.
JDiMatteo

Respuestas:

8

Al implementar funciones miembro const y no const que solo difieren en si el ptr / referencia devuelto es const, la mejor estrategia DRY es:

  1. si escribe un descriptor de acceso, considere si realmente lo necesita, consulte la respuesta de cmaster y http://c2.com/cgi/wiki?AccessorsAreEvil
  2. simplemente duplique el código si es trivial (por ejemplo, solo devuelve un miembro)
  3. nunca use un const_cast para evitar la duplicación relacionada con const
  4. para evitar la duplicación no trivial, use una función const privada que devuelva un no const que las funciones públicas const y no const llaman

p.ej

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Llamemos a esto la función const privada que devuelve un patrón no const .

Esta es la mejor estrategia para evitar duplicaciones de una manera directa y al mismo tiempo permitir que el compilador realice comprobaciones potencialmente útiles e informe mensajes de error relacionados con el constante.

JDiMatteo
fuente
sus argumentos son bastante convincentes, pero estoy bastante desconcertado de cómo puede obtener una referencia no constante a algo de una constinstancia (a menos que la referencia sea a algo declarado mutable, o a menos que emplee un const_castpero en ambos casos no hay ningún problema para empezar ) También pude encontrar nada en la "función private const regresar patrón no constante" (si es que se pretende que sea una broma llamarlo patrón .... isnt divertido;)
IDCLEV 463035818
1
Aquí hay un ejemplo de compilación basado en el código de la pregunta: ideone.com/wBE1GB . Lo siento, no quise decirlo como una broma, pero quise darle un nombre aquí (en el improbable caso de que merezca un nombre), y actualicé la redacción en la respuesta para tratar de aclararlo. Han pasado algunos años desde que escribí esto, y no recuerdo por qué pensé que un ejemplo que pasaba una referencia en el constructor era relevante.
JDiMatteo
Gracias por el ejemplo, no tengo tiempo ahora, pero definitivamente volveré a ello. Aquí hay una respuesta que presenta el mismo enfoque y en los comentarios se han señalado problemas similares: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Sí, tiene razón: muchos programas de C ++ que intentan la corrección constante están en clara violación del principio DRY, e incluso el miembro privado que regresa sin constante es un poco demasiado complejo para su comodidad.

Sin embargo, se pierde una observación: la duplicación de código debido a la corrección constante solo es un problema si está dando acceso a otro código a sus miembros de datos. Esto en sí mismo es una violación de la encapsulación. En general, este tipo de duplicación de código ocurre principalmente en accesores simples (después de todo, le está dando acceso a miembros ya existentes, el valor de retorno generalmente no es el resultado de un cálculo).

Mi experiencia es que las buenas abstracciones no tienden a incluir accesores. En consecuencia, evito en gran medida este problema definiendo funciones miembro que realmente hacen algo, en lugar de simplemente proporcionar acceso a los miembros de datos; Intento modelar el comportamiento en lugar de los datos. Mi intención principal en esto es obtener algo de abstracción tanto de mis clases como de sus funciones miembro individuales, en lugar de simplemente usar mis objetos como contenedores de datos. Pero este estilo también es bastante exitoso al evitar las toneladas de accesos de una línea repetitivos constantes / no constantes que son tan comunes en la mayoría de los códigos.

cmaster - restablecer monica
fuente
Parece discutible si los accesos son buenos o no, por ejemplo, vea la discusión en c2.com/cgi/wiki?AccessorsAreEvil . En la práctica, independientemente de lo que piense de los accesores, las bases de código grandes a menudo los usan, y si los usan sería mejor adherirse al principio DRY. Así que creo que la pregunta merece más una respuesta que eso, no deberías preguntarla.
JDiMatteo
1
Definitivamente es una pregunta que vale la pena formular :-) Y ni siquiera negaré que necesita accesores de vez en cuando. Simplemente digo que un estilo de programación que no esté basado en accesores reduce en gran medida el problema. No resuelve el problema por completo, pero al menos es lo suficientemente bueno para mí.
cmaster - reinstalar monica