Pointeur vers le membre de données de classe «:: *»


243

Je suis tombé sur cet extrait de code étrange qui compile très bien:

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;
    return 0;
}

Pourquoi C ++ a-t-il ce pointeur vers un membre de données non statique d'une classe? Quelle est l'utilité de cet étrange pointeur dans du vrai code?


Voici où je l'ai trouvé, m'a confondu aussi ... mais a du sens maintenant: stackoverflow.com/a/982941/211160
HostileFork dit de ne pas faire confiance au SE

Réponses:


190

C'est un "pointeur vers un membre" - le code suivant illustre son utilisation:

#include <iostream>
using namespace std;

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;

    Car c1;
    c1.speed = 1;       // direct access
    cout << "speed is " << c1.speed << endl;
    c1.*pSpeed = 2;     // access via pointer to member
    cout << "speed is " << c1.speed << endl;
    return 0;
}

Quant à savoir pourquoi vous voudriez faire cela, eh bien cela vous donne un autre niveau d'indirection qui peut résoudre certains problèmes délicats. Mais pour être honnête, je n'ai jamais eu à les utiliser dans mon propre code.

Edit: je ne peux pas penser à part d'une utilisation convaincante pour les pointeurs vers les données des membres. Le pointeur sur les fonctions membres peut être utilisé dans des architectures enfichables, mais encore une fois, produire un exemple dans un petit espace me bat. Ce qui suit est mon meilleur essai (non testé) - une fonction Appliquer qui effectuerait un pré et post-traitement avant d'appliquer une fonction membre sélectionnée par l'utilisateur à un objet:

void Apply( SomeClass * c, void (SomeClass::*func)() ) {
    // do hefty pre-call processing
    (c->*func)();  // call user specified function
    // do hefty post-call processing
}

Les parenthèses autour c->*funcsont nécessaires car l' ->*opérateur a une priorité plus faible que l'opérateur d'appel de fonction.


3
Pourriez-vous montrer un exemple de situation délicate où cela est utile? Merci.
Ashwin Nanjappa

J'ai un exemple d'utilisation du pointeur sur membre dans une classe Traits dans une autre réponse SO .
Mike DeSimone

Un exemple consiste à écrire une classe de type "rappel" pour un système basé sur des événements. Le système d'abonnement aux événements de l'interface utilisateur de CEGUI, par exemple, prend un rappel basé sur un modèle qui stocke un pointeur vers une fonction membre de votre choix, afin que vous puissiez spécifier une méthode pour gérer l'événement.
Benji XVI

2
Il y a un exemple assez cool d'utilisation du pointeur vers les données- membre dans une fonction de modèle dans ce code
alveko

3
J'ai récemment utilisé des pointeurs vers des membres de données dans un cadre de sérialisation. L'objet marshaller statique a été initialisé avec une liste d'encapsuleurs contenant un pointeur vers des membres de données sérialisables. Un premier prototype de ce code.
Alexey Biryukov, le

79

Ceci est l'exemple le plus simple auquel je puisse penser et qui traduit les rares cas où cette fonctionnalité est pertinente:

#include <iostream>

class bowl {
public:
    int apples;
    int oranges;
};

int count_fruit(bowl * begin, bowl * end, int bowl::*fruit)
{
    int count = 0;
    for (bowl * iterator = begin; iterator != end; ++ iterator)
        count += iterator->*fruit;
    return count;
}

int main()
{
    bowl bowls[2] = {
        { 1, 2 },
        { 3, 5 }
    };
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::apples) << " apples\n";
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::oranges) << " oranges\n";
    return 0;
}

La chose à noter ici est le pointeur passé à count_fruit. Cela vous évite d'avoir à écrire des fonctions count_apples et count_oranges distinctes.


3
Ne devrait-il pas en être &bowls.applesainsi &bowls.oranges? &bowl::appleset &bowl::orangesne pointe vers rien.
Dan Nissenbaum

19
&bowl::appleset &bowl::orangesne pointez pas sur les membres d'un objet ; ils désignent les membres d'une classe . Ils doivent être combinés avec un pointeur sur un objet réel avant de pointer vers quelque chose. Cette combinaison est réalisée avec l' ->*opérateur.
John McFarlane

58

Une autre application sont les listes intrusives. Le type d'élément peut indiquer à la liste quels sont ses pointeurs suivant / précédent. La liste n'utilise donc pas de noms codés en dur mais peut toujours utiliser des pointeurs existants:

// say this is some existing structure. And we want to use
// a list. We can tell it that the next pointer
// is apple::next.
struct apple {
    int data;
    apple * next;
};

// simple example of a minimal intrusive list. Could specify the
// member pointer as template argument too, if we wanted:
// template<typename E, E *E::*next_ptr>
template<typename E>
struct List {
    List(E *E::*next_ptr):head(0), next_ptr(next_ptr) { }

    void add(E &e) {
        // access its next pointer by the member pointer
        e.*next_ptr = head;
        head = &e;
    }

    E * head;
    E *E::*next_ptr;
};

int main() {
    List<apple> lst(&apple::next);

    apple a;
    lst.add(a);
}

Si c'est vraiment une liste chaînée, ne voudriez-vous pas quelque chose comme ceci: void add (E * e) {e -> * next_ptr = head; tête = e; } ??
eeeeaaii

4
@eee Je vous recommande de lire les paramètres de référence. Ce que j'ai fait est fondamentalement équivalent à ce que vous avez fait.
Johannes Schaub - litb

+1 pour votre exemple de code, mais je n'ai pas vu la nécessité d'utiliser le pointeur sur membre, un autre exemple?
Alcott

3
@Alcott: Vous pouvez l'appliquer à d'autres structures de type liste liée où le pointeur suivant n'est pas nommé next.
icktoofay

41

Voici un exemple concret sur lequel je travaille en ce moment, à partir des systèmes de traitement / contrôle du signal:

Supposons que vous ayez une structure qui représente les données que vous collectez:

struct Sample {
    time_t time;
    double value1;
    double value2;
    double value3;
};

Supposons maintenant que vous les bourriez dans un vecteur:

std::vector<Sample> samples;
... fill the vector ...

Supposons maintenant que vous vouliez calculer une fonction (disons la moyenne) d'une des variables sur une plage d'échantillons, et que vous souhaitiez factoriser ce calcul moyen dans une fonction. Le pointeur sur membre simplifie:

double Mean(std::vector<Sample>::const_iterator begin, 
    std::vector<Sample>::const_iterator end,
    double Sample::* var)
{
    float mean = 0;
    int samples = 0;
    for(; begin != end; begin++) {
        const Sample& s = *begin;
        mean += s.*var;
        samples++;
    }
    mean /= samples;
    return mean;
}

...
double mean = Mean(samples.begin(), samples.end(), &Sample::value2);

Note modifiée le 08/08/2016 pour une approche modèle-fonction plus concise

Et, bien sûr, vous pouvez le modéliser pour calculer une moyenne pour tout itérateur direct et tout type de valeur qui prend en charge l'addition avec lui-même et la division par size_t:

template<typename Titer, typename S>
S mean(Titer begin, const Titer& end, S std::iterator_traits<Titer>::value_type::* var) {
    using T = typename std::iterator_traits<Titer>::value_type;
    S sum = 0;
    size_t samples = 0;
    for( ; begin != end ; ++begin ) {
        const T& s = *begin;
        sum += s.*var;
        samples++;
    }
    return sum / samples;
}

struct Sample {
    double x;
}

std::vector<Sample> samples { {1.0}, {2.0}, {3.0} };
double m = mean(samples.begin(), samples.end(), &Sample::x);

EDIT - Le code ci-dessus a des implications sur les performances

Vous devriez noter, comme je l'ai rapidement découvert, que le code ci-dessus a de sérieuses implications en termes de performances. Le résumé est que si vous calculez une statistique récapitulative sur une série temporelle ou calculez une FFT, etc., vous devez stocker les valeurs de chaque variable de manière contiguë en mémoire. Sinon, l'itération sur la série entraînera un échec de cache pour chaque valeur récupérée.

Considérez les performances de ce code:

struct Sample {
  float w, x, y, z;
};

std::vector<Sample> series = ...;

float sum = 0;
int samples = 0;
for(auto it = series.begin(); it != series.end(); it++) {
  sum += *it.x;
  samples++;
}
float mean = sum / samples;

Sur de nombreuses architectures, une instance de Sampleremplira une ligne de cache. Ainsi, à chaque itération de la boucle, un échantillon sera extrait de la mémoire dans le cache. 4 octets de la ligne de cache seront utilisés et le reste jeté, et la prochaine itération entraînera un autre échec de cache, un accès à la mémoire, etc.

Il vaut mieux faire cela:

struct Samples {
  std::vector<float> w, x, y, z;
};

Samples series = ...;

float sum = 0;
float samples = 0;
for(auto it = series.x.begin(); it != series.x.end(); it++) {
  sum += *it;
  samples++;
}
float mean = sum / samples;

Maintenant, lorsque la première valeur x est chargée à partir de la mémoire, les trois suivantes seront également chargées dans le cache (en supposant un alignement approprié), ce qui signifie que vous n'avez pas besoin de valeurs chargées pour les trois prochaines itérations.

L'algorithme ci-dessus peut être amélioré un peu plus en utilisant des instructions SIMD, par exemple sur les architectures SSE2. Cependant, ceux-ci fonctionnent beaucoup mieux si les valeurs sont toutes contiguës en mémoire et que vous pouvez utiliser une seule instruction pour charger quatre échantillons ensemble (davantage dans les versions SSE ultérieures).

YMMV - concevez vos structures de données en fonction de votre algorithme.


C'est excellent. Je suis sur le point d'implémenter quelque chose de très similaire, et maintenant je n'ai plus à comprendre l'étrange syntaxe! Merci!
Nicu Stiurca

C'est la meilleure réponse. La double Sample::*partie est la clé!
Eyal

37

Vous pouvez accéder ultérieurement à ce membre, sur n'importe quelle instance:

int main()
{    
  int Car::*pSpeed = &Car::speed;    
  Car myCar;
  Car yourCar;

  int mySpeed = myCar.*pSpeed;
  int yourSpeed = yourCar.*pSpeed;

  assert(mySpeed > yourSpeed); // ;-)

  return 0;
}

Notez que vous avez besoin d'une instance pour l'appeler, elle ne fonctionne donc pas comme un délégué.
Il est rarement utilisé, j'en ai eu besoin une ou deux fois au cours de toutes mes années.

Normalement, utiliser une interface (c'est-à-dire une classe de base pure en C ++) est le meilleur choix de conception.


Mais ce n'est sûrement qu'une mauvaise pratique? devrait faire quelque chose comme youcar.setspeed (mycar.getpspeed)
thecoshman

9
@thecoshman: dépend entièrement - cacher les membres de données derrière les méthodes set / get n'est pas de l'encapsulation et simplement une tentative de laitière pour l'abstraction de l'interface. Dans de nombreux scénarios, la «dénormalisation» aux membres du public est un choix raisonnable. Mais cette discussion dépasse probablement les limites de la fonctionnalité de commentaire.
peterchen

4
+1 pour avoir souligné, si je comprends bien, qu'il s'agit d'un pointeur vers un membre d'une instance, et non un pointeur vers une valeur spécifique d'une instance, qui est la partie qui me manquait complètement.
johnbakers

@Fellowshee Vous comprenez bien :) (a souligné cela dans la réponse).
peterchen

26

IBM a plus de documentation sur la façon de l'utiliser. En bref, vous utilisez le pointeur comme décalage dans la classe. Vous ne pouvez pas utiliser ces pointeurs en dehors de la classe à laquelle ils se réfèrent, donc:

  int Car::*pSpeed = &Car::speed;
  Car mycar;
  mycar.*pSpeed = 65;

Cela semble un peu obscur, mais une application possible est si vous essayez d'écrire du code pour désérialiser des données génériques dans de nombreux types d'objets différents, et que votre code doit gérer des types d'objets dont il ne sait absolument rien (par exemple, votre code est dans une bibliothèque, et les objets dans lesquels vous désérialisez ont été créés par un utilisateur de votre bibliothèque). Les pointeurs membres vous donnent un moyen générique et semi-lisible de faire référence aux décalages individuels des membres de données, sans avoir à recourir à des astuces void * sans type comme vous le feriez pour les structures C.


Pourriez-vous partager un exemple d'extrait de code où cette construction est utile? Merci.
Ashwin Nanjappa

2
Je fais actuellement beaucoup de cela en raison de certains travaux DCOM et de l'utilisation de classes de ressources gérées, ce qui implique de faire un peu de travail avant chaque appel, et d'utiliser des membres de données pour la représentation interne à envoyer à com, ainsi que des modèles, fait beaucoup de code de la plaque de la chaudière beaucoup plus petit
Dan

19

Il permet de lier les variables et fonctions membres de manière uniforme. Voici un exemple avec votre classe de voiture. Une utilisation plus courante serait contraignante std::pair::firstet ::secondlors de l'utilisation dans les algorithmes STL et Boost sur une carte.

#include <list>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>


class Car {
public:
    Car(int s): speed(s) {}
    void drive() {
        std::cout << "Driving at " << speed << " km/h" << std::endl;
    }
    int speed;
};

int main() {

    using namespace std;
    using namespace boost::lambda;

    list<Car> l;
    l.push_back(Car(10));
    l.push_back(Car(140));
    l.push_back(Car(130));
    l.push_back(Car(60));

    // Speeding cars
    list<Car> s;

    // Binding a value to a member variable.
    // Find all cars with speed over 60 km/h.
    remove_copy_if(l.begin(), l.end(),
                   back_inserter(s),
                   bind(&Car::speed, _1) <= 60);

    // Binding a value to a member function.
    // Call a function on each car.
    for_each(s.begin(), s.end(), bind(&Car::drive, _1));

    return 0;
}

11

Vous pouvez utiliser un tableau de pointeurs vers des données de membre (homogènes) pour activer une interface double, membre nommé (iexdata) et tableau-indice (ie x [idx]).

#include <cassert>
#include <cstddef>

struct vector3 {
    float x;
    float y;
    float z;

    float& operator[](std::size_t idx) {
        static float vector3::*component[3] = {
            &vector3::x, &vector3::y, &vector3::z
        };
        return this->*component[idx];
    }
};

int main()
{
    vector3 v = { 0.0f, 1.0f, 2.0f };

    assert(&v[0] == &v.x);
    assert(&v[1] == &v.y);
    assert(&v[2] == &v.z);

    for (std::size_t i = 0; i < 3; ++i) {
        v[i] += 1.0f;
    }

    assert(v.x == 1.0f);
    assert(v.y == 2.0f);
    assert(v.z == 3.0f);

    return 0;
}

J'ai plus souvent vu cela implémenté en utilisant une union anonyme comprenant un champ tableau v [3] car cela évite une indirection, mais néanmoins intelligent, et potentiellement utile pour les champs non contigus.
Dwayne Robinson

2
@DwayneRobinson mais utiliser un unionpour taper-pun de cette manière n'est pas autorisé par la norme car il invoque de nombreuses formes de comportement indéfini ... alors que cette réponse est correcte.
underscore_d

C'est un bon exemple, mais l'opérateur [] peut être réécrit sans pointeur vers le composant: float *component[] = { &x, &y, &z }; return *component[idx];c'est -à- dire que le pointeur vers le composant semble ne servir à rien sauf à l'obscurcissement.
tobi_s

2

Une façon dont je l'ai utilisé est si j'ai deux implémentations sur la façon de faire quelque chose dans une classe et que je veux en choisir une au moment de l'exécution sans avoir à parcourir continuellement une instruction if, c'est-à-dire

class Algorithm
{
public:
    Algorithm() : m_impFn( &Algorithm::implementationA ) {}
    void frequentlyCalled()
    {
        // Avoid if ( using A ) else if ( using B ) type of thing
        (this->*m_impFn)();
    }
private:
    void implementationA() { /*...*/ }
    void implementationB() { /*...*/ }

    typedef void ( Algorithm::*IMP_FN ) ();
    IMP_FN m_impFn;
};

De toute évidence, cela n'est utile que si vous sentez que le code est suffisamment martelé pour que l'instruction if ralentisse les choses, par exemple. profondément dans les entrailles d'un algorithme intensif quelque part. Je pense toujours qu'elle est plus élégante que la déclaration if même dans des situations où elle n'a aucune utilité pratique, mais ce n'est que mon opinion.


Fondamentalement, vous pouvez obtenir la même chose avec l'abstrait Algorithmet deux classes dérivées, par exemple, AlgorithmAet AlgorithmB. Dans un tel cas, les deux algorithmes sont bien séparés et sont garantis pour être testés indépendamment.
shycha

2

Les pointeurs vers les classes ne sont pas de vrais pointeurs; une classe est une construction logique et n'a aucune existence physique en mémoire, cependant, lorsque vous construisez un pointeur vers un membre d'une classe, cela donne un décalage dans un objet de la classe du membre où le membre peut être trouvé; Cela donne une conclusion importante: puisque les membres statiques ne sont associés à aucun objet, un pointeur vers un membre NE PEUT PAS pointer vers un membre statique (données ou fonctions) que ce soit. Tenez compte des points suivants:

class x {
public:
    int val;
    x(int i) { val = i;}

    int get_val() { return val; }
    int d_val(int i) {return i+i; }
};

int main() {
    int (x::* data) = &x::val;               //pointer to data member
    int (x::* func)(int) = &x::d_val;        //pointer to function member

    x ob1(1), ob2(2);

    cout <<ob1.*data;
    cout <<ob2.*data;

    cout <<(ob1.*func)(ob1.*data);
    cout <<(ob2.*func)(ob2.*data);


    return 0;
}

Source: La référence complète C ++ - Herbert Schildt 4e édition


0

Je pense que vous ne voudriez le faire que si les données des membres étaient assez grandes (par exemple, un objet d'une autre classe assez lourde), et que vous avez une routine externe qui ne fonctionne que sur les références aux objets de cette classe. Vous ne voulez pas copier l'objet membre, cela vous permet donc de le faire circuler.


0

Voici un exemple où un pointeur vers des membres de données pourrait être utile:

#include <iostream>
#include <list>
#include <string>

template <typename Container, typename T, typename DataPtr>
typename Container::value_type searchByDataMember (const Container& container, const T& t, DataPtr ptr) {
    for (const typename Container::value_type& x : container) {
        if (x->*ptr == t)
            return x;
    }
    return typename Container::value_type{};
}

struct Object {
    int ID, value;
    std::string name;
    Object (int i, int v, const std::string& n) : ID(i), value(v), name(n) {}
};

std::list<Object*> objects { new Object(5,6,"Sam"), new Object(11,7,"Mark"), new Object(9,12,"Rob"),
    new Object(2,11,"Tom"), new Object(15,16,"John") };

int main() {
    const Object* object = searchByDataMember (objects, 11, &Object::value);
    std::cout << object->name << '\n';  // Tom
}

0

Supposons que vous ayez une structure. À l'intérieur de cette structure se trouvent * une sorte de nom * deux variables du même type mais avec une signification différente

struct foo {
    std::string a;
    std::string b;
};

Bon, maintenant disons que vous avez un tas de foos dans un conteneur:

// key: some sort of name, value: a foo instance
std::map<std::string, foo> container;

Bon, supposons maintenant que vous chargez les données à partir de sources distinctes, mais les données sont présentées de la même manière (par exemple, vous avez besoin de la même méthode d'analyse).

Vous pouvez faire quelque chose comme ça:

void readDataFromText(std::istream & input, std::map<std::string, foo> & container, std::string foo::*storage) {
    std::string line, name, value;

    // while lines are successfully retrieved
    while (std::getline(input, line)) {
        std::stringstream linestr(line);
        if ( line.empty() ) {
            continue;
        }

        // retrieve name and value
        linestr >> name >> value;

        // store value into correct storage, whichever one is correct
        container[name].*storage = value;
    }
}

std::map<std::string, foo> readValues() {
    std::map<std::string, foo> foos;

    std::ifstream a("input-a");
    readDataFromText(a, foos, &foo::a);
    std::ifstream b("input-b");
    readDataFromText(b, foos, &foo::b);
    return foos;
}

À ce stade, l'appel readValues()renvoie un conteneur avec l'unisson de "input-a" et "input-b"; toutes les clés seront présentes et les foos auront a ou b ou les deux.


0

Juste pour ajouter quelques cas d'utilisation pour la réponse de @ anon et @ Oktalist, voici un excellent matériel de lecture sur la fonction pointeur vers membre et les données pointeur vers membre.

https://www.dre.vanderbilt.edu/~schmidt/PDF/C++-ptmf4.pdf


le lien est mort. C'est pourquoi aucune réponse de lien uniquement n'est attendue ici. Au moins résumez le contenu du lien, sinon votre réponse devient invalide lorsque le lien pourrit
phuclv
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.