Forum Programmation.c++ Appeler une méthode non-const à partir de la méthode const homonyme

Posté par . Licence CC by-sa
Tags :
2
8
jan.
2014

Histoire de ne pas mourir idiot, je me demandais s'il y avait un moyen élégant d'appeler une méthode const à partir de son équivalent non-const. Le contexte ressemble à ça:

#include <vector>
#include <iostream>
#include <cassert>

using namespace std;

class A {
   public: 
      A(vector<double>);
      double mean();
      double mean() const;

   protected:
      void initialize();
      bool is_initialized;
      const vector<double> data;
      double sum_i;
};

A::A(vector<double> d) 
   : data(d) 
{
    sum_i = 0.0;
    is_initialized = false;
}

void A::initialize()
{
   for (unsigned int i = 0; i < data.size(); i++) {
       sum_i += data[i];
   }
   is_initialized = true;
}

double A::mean() const
{
    assert(is_initialized);
    return(sum_i / data.size());
}

double A::mean()
{
    if (!is_initialized) {
        initialize();
    }
    return(static_cast<const A>(*this).mean()); // <- c'est là qu'est l'os!
}

int main()
{
   vector<double> notes;
   notes.push_back(10.0);
   notes.push_back(13.0);
   notes.push_back(3.5);

   A A1(notes);
   cout << A1.mean() << endl;

   const A A2(A1);
   cout << A2.mean() << endl;

   return(0);
}

Je ne sais pas si je préfère le cast vers le const ou la duplication de code, les deux me semblent mochissimes. Est-ce une mauvaise utilisation de l'overloading sur const? J'avais pourtant l'impression que c'était légitime dans ce contexte (besoin d'un état particulier de l'objet pour exécuter la fonction; mettre l'objet en état et lancer la fonction const dans la fonction non-const, avorter l'exécution dans un contexte const si l'objet n'est pas dans le bon état).

  • # const→non-const ou non-const→const ?

    Posté par . Évalué à 2.

    Hello,

    Dans un premier temps, le code que tu mets en évidence semble faire le contraire du titre de ton billet : appeler la fonction-membre constante depuis la non constante, ce qui est effectivement préférable à l'inverse qui t'obligerait à créer une copie de l'objet.

    Ensuite, n'est-il pas plus judicieux de ne transtyper que le pointeur et pas l'objet pointé entier ? Genre return (const_cast<A const *>this)->mean();

    • [^] # Re: const→non-const ou non-const→const ?

      Posté par . Évalué à 2.

      Ah oui oui, je me suis emmêlé les pinceaux dans le titre, toutes mes confuses.

      Je ne sais pas trop les conséquences du cast, tu veux dire qu'un static_cast sur *this appelle le constructeur de copie?

    • [^] # Re: const→non-const ou non-const→const ?

      Posté par . Évalué à 3.

      Faut pas utiliser un const_cast quand on en a pas besoin. const_cast permet de passer du pas-const à du const, or t'a pas envie de permettre ça, même lorsque tu caste du pas-const en const.

      Non, une simple conversion implicite suffit, et c'est plus lisible:

      const A& me = *this;
      return me.mean();
      • [^] # Re: const→non-const ou non-const→const ?

        Posté par . Évalué à 2.

        Ah oui, en effet, c'est nettement moins laid.

        Ceci dit, j'ai quand même l'impression que ça revient à utiliser une "faille" dans le langage pour faire un truc qui n'est pas prévu, et donc qui n'est pas canonique. Je me demande si, au final, il n'est pas plus sain de déporter les calculs dans une fonction const qui a un autre nom, et d'utiliser les fonctions overloadées comme des coquilles vides :

        class A 
        {
          public:
        ...
           double mean() {if (!is_initialized) initialize(); return(compute_mean());}
           double mean() const {if (!is_initialize) abort(); return(compute_mean());}
         protected:
        ...
           double compute_mean() const {return(sum_i/data.size());}
        };
        

        Il y a une fonction de plus, mais finalement, C++ est un langage verbeux, si on aime la concision on change de langage… Et ça permet de faciliter la lecture de toute manière, puisque la fonction appelée est explicite. J'imagine qu'on peut même imposer en convention de codage de l'appeler const_mean(), un peu à la manière des const_iterator de la lib stantard. Merci pour l'éclairage, en tout cas.

  • # 'mutable' ?

    Posté par (page perso) . Évalué à 4. Dernière modification le 08/01/14 à 18:22.

    Je ne suis pas un spécialiste du C++ 'standard', mais, de manière générale, une méthode 'const' ne devrait appeler que des méthodes 'const'.

    Je m'étais moi-même trouvé dans une situation ou une méthode, qui ne modifiait pas fondamentalement l'objet sur lequel elle s'appliquait, d'où sa qualification en 'const', modifiait néanmoins un de ses membres (dans mon cas, c'était un cache), ce que le compilateur ne manqua pas de me faire remarquer de manière péremptoire tout en refusant de mener à terme son travail. Pour résoudre ce problème, j'ai déclaré le cache en question en 'mutable', ce qui m'a permis de laisser la méthode en 'const', à la grande satisfaction de mon compilateur, qui ne pipa plus mot.

    Concrètement, pour le code présenté, pour ce que j'en ai compris, je déclarerais 'is_initialzed' en 'mutable', ce qui permettrait de déclarer 'initialize()' en 'const', et il n'y aurait plus besoin que d'une seule méthode 'mean' déclarée en 'const'. Ça devrait fonctionner, mais je ne sais pas si conceptuellement ça tient la route, car un 'initialize()' en 'const' me semble bizarre…

    Freelance en ingénierie informatique.

    • [^] # Re: 'mutable' ?

      Posté par (page perso) . Évalué à 1. Dernière modification le 09/01/14 à 10:15.

      Je plusoie l'utilisation du mutable, notamment dans ce cas.

      Le mutable doit être utilisé dans le cas de "fausses données", des données qui peuvent être (et sont) calculées à partir de "vrai données" (cache, debug, variable intermédiaire et autres vicieuseries).

      C'est clairement le cas ici. sum_i dépend entièrement de data (qui en plus est const). Je mettrai donc sum_i et is_initialized en mutable (et private).

      Après, de manière plus générale et pour ne pas répondre à ta question; vu que data est constant, sum_i l'est aussi. Tu devrais aussi le mettre en const est le calculer dans le constructeur. (Mais l'exemple que tu as donné n'est peut être qu'un cas simplifié de ton vrai problème)

      Matthieu Gautier|irc:starmad

      • [^] # Re: 'mutable' ?

        Posté par . Évalué à 2.

        Je ne suis pas certain que j'ai compris "mutable" de la même manière. Je n'aurais tendance à utiliser "mutable" uniquement dans des cas très spécifiques, par exemple pour pouvoir modifier un cache, ou quelque chose comme ça. Si les propriétés de l'objet sont modifiées (ici, elles le sont clairement, vu que l'objet passe de !is_initialized à is_initialized, et que le comportement de l'objet change (il appelle abort() ou il retourne un résultat quand appelé en contexte const), je trouve que l'utilisation de 'mutable' est abusif.

        Tu devrais aussi le mettre en const est le calculer dans le constructeur.

        Je ne sais pas, évidemment, dans le cas réel, il y a plus de trucs à calculer. Imagine que tu puisse demander aussi la variance ; si tu calcules tout dans le constructeur, tu vas devoir calculer sum_i et sum_i2, et si tu ne demandes finalement que la moyenne, tu auras calculé sum_i2 pour rien. J'avoue que c'est une question non triviale, mais ça me semble une pessimisation a priori que de faire des calculs potentiellement lourds alors qu'on n'est pas certain d'utiliser le résultat.

        • [^] # Re: 'mutable' ?

          Posté par . Évalué à 2. Dernière modification le 09/01/14 à 12:51.

          si il y a plusieurs truc que la moyenne, j'utiliserai un

          private:
          mutable int cached_values=0;

          et pour voir si une valeur est en cache

          if( !( CACHE_MEAN & cached_values ) ) 
            return _mean(); 
          return mean_;

          avec

          float _mean(){ cache_mean_ = sum() / vect.size() ; cached_values |= CACHE_MEAN ; return mean_; }

          et lors de toutes modification du vecteur, cached_values=0;

          Par contre si is_initialised dit juste si le cache est à jour, c'est une variable très mal nommée. Quant à avoir un comportement différent selon qu'on soit const ou pas const (abort ou moyenne), je trouve ça très moyen;

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

        • [^] # Re: 'mutable' ?

          Posté par (page perso) . Évalué à 1.

          Je ne suis pas certain que j'ai compris "mutable" de la même manière.

          Je pense qu'on a compris mutable de la même manière. C'est la vision qu'on a de ta classe qui diffère :)

          Pour moi, la classe A est surtout un conteneur de double (stocké dans data).
          sum_i et is_initialized sont des variables internes :

          • sum_i qui stocke un calcul intermédiaire - potentiellement lourd, donc tu veux déférer le calcul que lorsque c'est nécessaire et stocké le résultat.
          • is_initialized qui n'est qu'un flag pour savoir si le calcul intermédiaire a été fait.

          Pour le comportement différent const/non const, je rejoint fearan : le comportement devrait être identique.

          Dans le contexte que je viens de décrire, il est tout a fait valable de mettre ces variables internes en mutable : Le "contrat" de ta classe (et de ta méthode const) n'est pas cassé, il ne modifie pas les "vrais" donnés (data).

          Je ne sais pas, évidemment, dans le cas réel, il y a plus de trucs à calculer. Imagine que tu puisse demander aussi la variance […]

          Tout à fait. Tu remarqueras que j'ai pris mes précautions et que je parle bien du code que tu as fourni, pas du problème réel.


          Je compléterai aussi la réponse de fearan en mettant le test CACHE_MEAN & cached_values directement dans _mean() et en appelant tout le temps cette dernière.

          Matthieu Gautier|irc:starmad

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.