Const C ++ DRY Stratégies

14

Pour éviter la duplication non triviale liée à la const C ++, existe-t-il des cas où const_cast fonctionnerait mais une fonction const privée renvoyant non const ne le serait pas?

Dans l' article efficace C ++ de Scott Meyers 3, il suggère qu'un const_cast combiné avec un transtypage statique peut être un moyen efficace et sûr d'éviter le code en double, par exemple

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 poursuit en expliquant que le fait d'appeler la fonction const la fonction non-const est dangereux.

Le code ci-dessous est un contre-exemple montrant:

  • contrairement à la suggestion de Meyers, parfois le const_cast combiné avec un cast statique est dangereux
  • avoir parfois la fonction const appeler le non-const est moins dangereux
  • parfois dans les deux sens en utilisant un const_cast masquer les erreurs de compilation potentiellement utiles
  • éviter un const_cast et avoir un membre privé const supplémentaire retournant un non-const est une autre option

L'une des stratégies const_cast pour éviter la duplication de code est-elle considérée comme une bonne pratique? Préférez-vous plutôt la stratégie de méthode privée? Y a-t-il des cas où const_cast fonctionnerait mais pas une méthode privée? Existe-t-il d'autres options (outre la duplication)?

Ma préoccupation avec les stratégies const_cast est que même si le code est correct lors de l'écriture, plus tard pendant la maintenance, le code pourrait devenir incorrect et le const_cast cacherait une erreur de compilation utile. Il semble qu'une fonction privée commune soit généralement plus sûre.

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
la source
Un getter qui retourne juste un membre est plus court que celui qui lance et appelle l'autre version de lui-même. L'astuce est destinée aux fonctions plus compliquées où le gain de déduplication l'emporte sur les risques de cast.
Sebastian Redl
@SebastianRedl Je suis d'accord pour dire que la duplication serait meilleure si je revenais juste membre. Veuillez imaginer que c'est plus compliqué, par exemple au lieu de retourner mConstLongLived, nous pourrions appeler une fonction sur mConstLongLived qui retourne une référence const qui est ensuite utilisée pour appeler une autre fonction qui retourne une référence const que nous ne possédons pas et n'avons accès qu'à une version const de. J'espère que le point est clair que le const_cast peut supprimer const de quelque chose auquel nous n'aurions pas autrement accès non const.
JDiMatteo
4
Tout cela semble ridicule avec des exemples simples, mais la duplication liée à const apparaît dans le code réel, les erreurs du compilateur const sont utiles dans la pratique (souvent pour détecter des erreurs stupides), et je suis surpris que la solution "C ++ efficace" proposée soit une étrange et une paire de moulages apparemment sujette aux erreurs. Un membre const privé renvoyant un non-const semble clairement supérieur à un double casting, et je veux savoir s'il manque quelque chose.
JDiMatteo

Réponses:

8

Lors de l'implémentation de fonctions membres const et non const qui ne diffèrent que par le fait que le ptr / référence retourné est const, la meilleure stratégie DRY consiste à:

  1. si vous écrivez un accesseur, demandez-vous si vous en avez vraiment besoin, voir la réponse de cmaster et http://c2.com/cgi/wiki?AccessorsAreEvil
  2. il suffit de dupliquer le code s'il est trivial (par exemple, simplement renvoyer un membre)
  3. n'utilisez jamais un const_cast pour éviter la duplication liée à const
  4. pour éviter la duplication non triviale, utilisez une fonction const privée renvoyant un non const que les fonctions publiques const et non const appellent

par exemple

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

Appelons cela la fonction const privée retournant un motif non const .

Il s'agit de la meilleure stratégie pour éviter les doublons de manière simple tout en permettant au compilateur d'effectuer des vérifications potentiellement utiles et de signaler des messages d'erreur liés à const.

JDiMatteo
la source
vos arguments sont plutôt convaincants, mais je suis plutôt perplexe de voir comment vous pouvez obtenir une référence non const à quelque chose à partir d'une constinstance (à moins que la référence ne soit à quelque chose de déclaré mutable, ou à moins que vous n'utilisiez un const_castmais dans les deux cas il n'y a pas de probkem pour commencer ). De plus, je n'ai rien trouvé sur la "fonction const privée renvoyant un motif non const" (si c'était une blague pour l'appeler motif ... ce n'est pas drôle;)
idclev 463035818
1
Voici un exemple de compilation basé sur le code de la question: ideone.com/wBE1GB . Désolé, je ne le pensais pas comme une blague, mais je voulais lui donner un nom ici (dans le cas peu probable qu'il mérite un nom), et j'ai mis à jour le libellé de la réponse pour essayer de le clarifier. Cela fait quelques années que j'ai écrit ceci, et je ne me souviens pas pourquoi je pensais qu'un exemple passant une référence dans le constructeur était pertinent.
JDiMatteo
Merci pour l'exemple, je n'ai pas le temps maintenant, mais j'y reviendrai certainement. Voici une réponse qui présente la même approche et dans les commentaires des problèmes similaires ont été signalés: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Oui, vous avez raison: de nombreux programmes C ++ qui tentent de corriger la const sont en violation flagrante du principe DRY, et même le membre privé qui retourne non const est un peu trop complexe pour le confort.

Cependant, vous manquez une observation: la duplication de code en raison de la const-correct n'est toujours un problème que si vous donnez à d'autres membres l'accès au code. Cela en soi est en violation de l'encapsulation. Généralement, ce type de duplication de code se produit principalement dans des accesseurs simples (après tout, vous donnez accès à des membres déjà existants, la valeur de retour n'est généralement pas le résultat d'un calcul).

D'après mon expérience, les bonnes abstractions n'ont pas tendance à inclure les accesseurs. Par conséquent, j'évite largement ce problème en définissant des fonctions membres qui font réellement quelque chose, plutôt que de simplement fournir un accès aux membres de données; J'essaie de modéliser le comportement au lieu des données. Mon intention principale dans ce cas est d'obtenir en fait une certaine abstraction de mes classes et de leurs fonctions membres individuelles, au lieu d'utiliser simplement mes objets comme conteneurs de données. Mais ce style réussit également très bien à éviter les tonnes d'accesseurs répétitifs const / non const qui sont si courants dans la plupart des codes.

cmaster - réintégrer monica
la source
Il semble possible de débattre de la qualité des accesseurs, par exemple, voir la discussion sur c2.com/cgi/wiki?AccessorsAreEvil . En pratique, peu importe ce que vous pensez des accesseurs, les grandes bases de code les utilisent souvent, et s’ils les utilisent, il serait préférable de respecter le principe DRY. Je pense donc que la question mérite plus de réponse que vous ne devriez pas la poser.
JDiMatteo
1
C'est certainement une question qui mérite d'être posée :-) Et je ne nierai même pas que vous avez besoin d'accesseurs de temps en temps. Je dis simplement qu'un style de programmation qui n'est pas basé sur des accesseurs réduit considérablement le problème. Cela ne résout pas complètement le problème, mais c'est au moins assez bon pour moi.
cmaster - réintègre monica le