[C++] Comprendre et savoir utiliser le pointeur this

Après lecture du chapître 10 du C++ Primer 5th

Le problème exposé dans ce sujet a été résolu.

Bonsoir à toutes et à tous !

C’est encore moi avec mon C++ Primer et mes questions #HommeLourd :-°

L’anglais ne me pose pas de problèmes mais j’avoue que plus je rentre dans les parties techniques du C++, plus l’incertitude s’installe.

J’ai compris le concept de base des classes (Les écrires, membres et méthodes, private et public, prog. modulaire, constructeur (implicite ou pas) et destructeur.)

En arrivant à la partie sur this, j’ai saisi le fait que ce pointeur particulier est fait pour contenir l’adresse de la classe ou il est utilisé.

Cependant, je ne comprends pas entièrement à quoi il va bien pouvoir me servir et comment l’utiliser correctement.

Voici une classe de test que j’ai écrite afin de pouvoir comprendre ce pointeur :

class.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef CLASS_H
#define CLASS_H

class Test
{
    private:
        std::string m_testVar;

    public:
        Test(std::string testVar);
        ~Test();

    void show();
    void modify(Test & x, std::string s);
};

#endif

class.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
#include "class.h"

Test::Test(std::string testVar) : m_testVar(testVar)
{

}

Test::~Test()
{

}

void Test::show()
{
    std::cout << m_testVar << std::endl;
}

void Test::modify(Test::Test & x, std::string s)
{
    this->m_testVar = s;
}

Explications : Je souhaite au cours du programme pouvoir modifier, après avoir instancié un objet ’Test’, la valeur de son seul membre m_testVar qui est de type std::string (Par exemple, j’instancie un objet ’Test’ avec une littérale "Salut les canards" puis grâce à la méthode .modify() je change pour la littérale "Coin Coin" en lui passant par référence son propre objet et bien sûr la littérale en question.)

  • Est-ce que c’est comme ça qu’on fait ?
  • Est-ce que par rapport à la norme on a le droit de modifier un membre privé ?
  • Est-ce que mon jargon est correct ^^ ? (Oui quitte à prendre des bonnes pratiques, autant faire d’une pierre deux coups)
  • Que pouvez vous me dire de plus sur ce pointeur ?

J’ai testé tout ça et ça fonctionne en tout cas, mais le compilateur me claque un warning dans la tronche et de ce que j’applique du cours de @gbdivers (Que je lis en parrallèle), c’est que les warnings ça fait chier. Je compile sous Debian Stretch par le biais de make avec le compilateur CLang 3.9 et la norme C++14. Pour le warning, CLang m’indique : class.cpp:20:32: warning: unused parameter 'x' [-Wunused-parameter] void Test::modify(Test::Test & x, std::string s), ce qui est vrai donc je pense que j’utilise mal la chose.

Merci pour vos réponses, et si je n’ai pas été clair dans mes explications faites-le moi savoir c’est important !

+0 -0

[…] en lui passant par référence son propre objet […]

p4radox

Pourquoi faire ça ? this est justement un pointeur vers l’objet en question.

Tu peux te représenter l’équivalent en langage C, là où il n’était pas encore possible de déclarer des fonctions membre d’une classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Test
{
    int m_testVar = 0;
}

// fonction libre, on déclare 'this' explicitement pour l'exemple
void modify(Test * this, int i)
{
    this->m_testVar = i;
}

// ...

Test t;
modify(t, 3);

Dans ton code, ta référence x ne sert donc à rien, elle fait la même chose que this. En plus de cela, il n’est pas nécessaire d’appeler this pour accéder aux variables membres d’une classe depuis une de ses fonctions membres.

Finalement, pense à faire passer les string par références pour éviter les copies.

Au final, on se retrouve avec ça :

1
2
3
4
void Test::modify(const std::string & s)
{
    m_testVar = s;
}

Est-ce que par rapport à la norme on a le droit de modifier un membre privé ?

p4radox

Quelle norme ? Qui a le droit de modifier ?

Il y a pas de norme définie pour concevoir un programme, juste différents principes en fonction de différents paradigmes. Avant tout, c’est à toi de te demander si oui ou non c’est pertinent de modifier cette variable.

Je suis pas vraiment sûr de comprendre ta question.

Si tu comptes faire de la programmation orientée-objet, tu peux te pencher sur les principes SOLID. Ce livre développe assez bien ces principes.

Est-ce que mon jargon est correct ^^ ?

p4radox

Il faut savoir qu’un "membre" d’une classe désigne en fait tous les éléments de la classe (fonctions comprises). Dans le jargon C++, on utilise plus souvent "fonction membre" que "méthode".

Que pouvez vous me dire de plus sur ce pointeur ?

p4radox

Ce pointeur est notamment utile lorsque l’on veut faire passer l’objet qu’on manipule en paramètre d’une autre fonction. On ne le rencontre pas forcément souvent, mais avec certaines bibliothèques c’est un élément courant, par exemple avec Qt :

1
2
3
4
Window::machin()
{
    QMessageBox::critical(this, "Error", "Could not open file");
}

Ici, on a une fonction machin membre d’une classe qui représente une fenêtre. La fonction QMessageBox::critical demande en premier paramètre un pointeur vers la fenêtre parente de la boîte de message : c’est ici que this devient utile.

this est simplement un pointeur que tu peux utiliser dans les méthodes d’une classe qui pointe sur ton instance (l’objet depuis lequel tu appelles la méthode). C’est le même principe que this en Java ou en Javascript, ou self en Python.

Dans les méthodes d’une classe tu peux modifier les attributs de l’instance, même privés (et heureusement). En fait un attribut privé ne peut être lu ou écrit que depuis la classe où il est défini. Un attribut protected peut l’être depuis la classe où il est défini et ses classes-filles (cf l’héritage, je ne sais pas si tu en es encore là). Et un attribut public peut l’être depuis n’importe où.

Sinon, ce n’est pas comme ça qu’on fait. Très bon réflexe de prêter attention aux warnings, car en l’occurrence celle-ci t’indique le problème : x ne sert à rien. Pourquoi prendre en paramètre une instance si ce n’est pas celle que tu modifies ? Tu modifies this, qui est de base dans ton scope car tu es dans une méthode de la classe Test, donc tu as automatiquement le pointeur this qui pointe sur ton instance. Pas besoin de passer une instance en paramètre.

1
2
3
4
void Test::modify(std::string s)
{
    this->m_testVar = s;
}
1
2
3
4
Test test("a");
test.show(); // affiche a
test.modify("b");
test.show(); // affiche b

Ensuite, ici tu n’as pas besoin de this. Les attributs de ta classe sont aussi automatiquement dans ton scope quand tu es dans une méthode de cette classe. Tu peux manipuler les attributs directement. Ca a exactement le même effet, mais c’est plus rapide à écrire :

1
2
3
4
void Test::modify(std::string s)
{
    m_testVar = s;
}

Le this n’est utile qu’en cas d’ambiguïté, par ex si tu avais appelé ton paramètre non pas s mais m_testVar aussi. Dans ce cas le seul moyen de faire la différence entre le paramètre et l’attribut, c’est d’accéder à l’attribut via this.

1
2
3
4
void Test::modify(std::string m_testVar)
{
    this->m_testVar = m_testVar;
}

C’est d’ailleurs pour cette raison qu’on a l’habitude de préfixer les attributs avec "m_" ou "_". Pour éviter les ambiguïtés avec des paramètres, notamment dans les constructeurs et les méthodes de modification, et donc pour éviter de devoir utiliser this.

Voilà. Tu verras sans doute plus tard d’autres trucs par rapport à this. Par ex quand tu vas faire des interfaces graphiques, souvent les composants d’une interface graphique sont rangés de manière arborescente, donc par ex si tu fais une classe Fenetre et que dans son constructeur tu instancies un Button, tu voudras indiquer au bouton que son composant parent est la fenêtre, donc tu lui passeras this en paramètre pour qu’il puisse stocker un pointeur sur son composant parent. Tu verras aussi d’autres trucs rigolos quand tu aborderas les pointeurs intelligents… Bref.

Sinon pour parler jargon, mettre les attributs en private et écrire des méthodes public pour les lire et écrire, ça s’appelle l’encapsulation. Une méthode qui retourne un attribut s’appelle un getter (accesseur en français), comme ta méthode show si elle retournait l’attribut au lieu de l’afficher. D’ailleurs souvent on évite de faire des affichages dans la classe, on fait un getter qui retourne l’attribut et on fait l’affichage à l’extérieur de la classe. Et une méthode qui modifie un attribut, comme ta méthode modify, s’appelle un setter (ou mutateur, mais qui utilise ces mots français sérieusement ? ^^). Quand tu as plusieurs attributs dans une classe c’est de bon ton d’avoir une nomenclature homogène pour ces getters et setters. Souvent pour un attribut nommé Toto, on appelle le getter getToto et le mutateur setToto. Bon après c’est affaire de goût, par ex dans le framework Qt les getters portent simplement le même nom que l’attribut (préfixé de is si c’est un booléen). Ce qui compte c’est de rester cohérent, de suivre la même règle partout au sein du même projet. ;)

EDIT : +1 sur Olybri pour :

1
2
3
4
void Test::modify(const std::string & s)
{
    m_testVar = s;
}

C’est vraiment la norme d’usage de faire comme ça, en const&. Ton setter n’a pas de raison de modifier le paramètre, mais uniquement l’instance, donc le paramètre en const. Et la référence pour éviter une copie inutile (tu as déjà une copie ici quand tu fais m_testVar = s, tu en aurais deux sans la référence). De même sur un getter l’usage c’est de déclarer la méthode en const pour indiquer qu’elle ne modifie pas l’objet. Mais tu verras sûrement tout ça par la suite.

+1 -0

Effectivement, en se représentant comment j’aurais fait en C, je vois que il n’y avait pas lieu de faire avec this mais plûtot comme ça : x.m_testVar = s;. Et là bah forcément y’a pas lieu d’avoir un ’unused’. (Bah oui quel con fais-je &gt;_&lt;)

Finalement, pense à faire passer les string par références pour éviter les copies.

[Olybri]

C’est pas faux, j’ai complètement zappé ça. Pas de copies et dans cet exemple rien ne doit pouvoir modifier la litérrale qui est passée.

Quelle norme ? Qui a le droit de modifier ?

[Olybri]

Ce que je voulait dire c’est que par rapport à ce qui est écrit dans la norme C++14, est-ce que ce serait illégal de modifier une variable membre par le biais d’une fonction membre. Mais la question ne se pose plus car ce serait stupide en fait que les fonctions membres n’ait pas accès aux membres d’une même classe

Avant tout, c’est à toi de te demander si oui ou non c’est pertinent de modifier cette variable.

[Olybri]

Bonne remarque, ça me semble tout de ce qu’il y a de plus logique en effet. En fait, je pense que je n’ai pas bien compris à quoi servait ce pointeur dit intelligent.

Si j’ai bien compris, pour t’embêter une dernière fois, le pointeur this est utile lorsque que l’on a besoin qu’un objet d’une classe A soit utilisé dans un objet d’une classe B donc ?

En tout cas merci beaucoup pour ta réponse Olybri.

Pour répondre à Society qui a posté pendant que je rédigeais ma réponse :

Merci pour ton intervention aussi qui m’a aussi aidé à comprendre un peu plus les choses. Donc pas besoin de x.m_varTest au final, j’ai compris.

Pour le const std::string & s c’est vrai que rien ne justifie la modification de la littérale en cours de route et non plus d’en faire une copie, je souhaitais modifier la variable membre directement.

Je ne suis pas encore sur l’héritage donc je ne pourrais pas t’en dire plus là-dessus mais en tout cas la POO ça me plaît, venant du C je commence à trouver le C++ plus pertinent quand on veut faire autre chose que de l’embarqué ou de l’OS.

En fait une fois le cours lu, je désirerais mettre tout ça en oeuvre avec la SFML, mais apprendre au fur et à mesure toutes les subtilités du C++ est intéressant afin de pouvoir dans le futur être polyvalent et aider les autres à mon tour ou participer à des projets open-source.

Merci beaucoup en tout cas les gars.

+0 -0

Lu’!

Dans le jargon C++, on utilise plus souvent "fonction membre" que "méthode".

Olybri

Pour être plus complet : le mot "method" est exactement présent 0 fois dans la norme C++, sémantiquement d’après Stroustrup, une méthode serait une fonction membre virtuelle. Le mot "attribute" est présent dans la norme C++ mais ne désigne absolument pas les variables membres.

Un attribut protected peut l’être depuis la classe où il est défini et ses classes-filles (cf l’héritage, je ne sais pas si tu en es encore là). Et un attribut public peut l’être depuis n’importe où.

Society

Ne faites pas de variables membres protégées. On peut distinguer trois cas : la variable intervient dans l’invariant de l’objet, elle est alors privée, la variable n’intervient pas dans l’invariant, mais sert de cache (mutable par exemple), elle est encore privée, la variable n’intervient pas dans l’invariant et n’est pas un cache non plus, rendez la publique, vous gagnerez votre temps.

Plus de détails ici : https://openclassrooms.com/forum/sujet/public-private-ou-protected?page=1#message-90447802 .

Concernant les getters/setters : why setters and getters are evil ? .

Une note pour const : la règle dans la norme c’est "ça concerne le truc immédiatement à gauche, sauf s’il n’y en a pas, auquel cas, c’est à droite". Les règles en "sauf si" c’est moche, donc il est pas mal de s’habituer à écrire :

1
string const& variable ;

Ne faites pas de variables membres protégées. On peut distinguer trois cas : la variable intervient dans l’invariant de l’objet, elle est alors privée, la variable n’intervient pas dans l’invariant, mais sert de cache (mutable par exemple), elle est encore privée, la variable n’intervient pas dans l’invariant et n’est pas un cache non plus, rendez la publique, vous gagnerez votre temps.

Ksass`Peuk

Je n’ai pas compris cette partie, je suis allé voir le lien mais je n’ai pas tout saisi non plus.

On va distinguer par cas. Le premier c’est "ma variable membre intervient dans l’invariant de l’objet". Par exemple, on peut avoir un code comme ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class A{
  int m_i; //invariant: i != 0
public:
  int foo(){ return INT_MAX/i; }
};

int main(){
  std::vector<std::unique_ptr<A>> v;
  //du code 

  for(auto & p : v) v->foo();
}

Ici, on voit pas mal le problème : si je mets la variable m_i avec une visibilité protégée, j’autorise une classe dérivée à aller faire n’importe quoi avec la variable. Y compris :

1
2
3
struct B : public A {
  B(){ m_i = 0; }
};

Résultat, dans le code précédent si l’utilisateur met un objet B dans ma liste de A (ce qu’il devrait être autorisé à faire car l’héritage public me dit que tout objet B est un objet A), l’invariant n’est pas respecté et un appel à foo() provoquera un crash. Et on risque de mettre du temps avant de comprendre que le problème ne vient pas de la classe A mais de la classe B.

Pour le second point, je viens en fait de me rendre compte qu’il correspond en fait au premier point. c’est juste que la classe a un invariant supplémentaire "ma variable de cache correspond à tout moment au dernier calcul d’une fonction donnée".

Puis ben le dernier cas, c’est le truc le plus basique : si une variable peut être modifiée sans contrôle dans la classe de base, elle peut aussi être modifiée sans contrôle dans la classe dérivée, donc pas besoin de protected.

Puis ben le dernier cas, c’est le truc le plus basique : si une variable peut être modifiée sans contrôle dans la classe de base, elle peut aussi être modifiée sans contrôle dans la classe dérivée, donc pas besoin de protected.

Bah non, tu peux vouloir que l’extérieur n’ait pas moyen d’accéder ou de modifier directement cette information.

Le protected est une protection par rapport à l’extérieur, mais en effet cela ne protège pas d’une modification interne par une classe dérivée, mais en même temps c’est son but.

De toute façon de base je trouve toujours les conventions de codage pris à la lettre comme très limitant. Ça doit servir de guide, mais il faut savoir s’adapter.

+0 -0

(1) Bah non, tu peux vouloir que l’extérieur n’ait pas moyen d’accéder ou de modifier directement cette information.

(2) De toute façon de base je trouve toujours les conventions de codage pris à la lettre comme très limitant.

Renault

(1) Hum, pour moi si c’est le cas, ça veut juste dire que la variable en question rentre en jeu dans un invariant de l’objet. J’ai du mal à imaginer le cas d’une variable qui devrait être invisible depuis l’extérieur sans raison autre que "mon objet fonctionnera plus si l’utilisateur fait n’importe quoi avec".

(2) Dans le résumé que j’ai mis sur OC, il faut pas oublier l’addendum final, c’est ce que j’y précise. Mais quand on débute, on a, à mon avis, pas souvent suffisamment de recul pour différencier "un petit peu de difficulté pour avoir un meilleur découplage par la suite" et "ça va vraiment être bloquant et empirer mon architecture".

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte