web-dev-qa-db-fra.com

Pointeur sur le membre de données de classe ":: *"

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

class Car
{
    public:
    int speed;
};

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

Pourquoi C++ a-t-il ce pointeur sur un membre de données non statique d'une classe? Qu'est-ce l'utilisation de cet étrange pointeur dans du code réel?

200
Ashwin Nanjappa

C'est un "pointeur sur 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;
}

En ce qui concerne pourquoi vous voudriez le faire, cela vous donne un autre niveau d’indirection qui peut résoudre certains problèmes épineux. Mais pour être honnête, je n'ai jamais eu à les utiliser dans mon propre code.

Edit: Je n'arrive pas à penser à une utilisation convaincante des pointeurs vers les données des membres. Le pointeur sur les fonctions membres peut être utilisé dans les architectures enfichables, mais une fois encore, produire un exemple dans un espace réduit me met en échec. Ce qui suit est mon meilleur essai (non testé) - une fonction Apply qui effectuerait un traitement préalable et ultérieur 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 de c->*func sont nécessaires car l'opérateur ->* a une priorité inférieure à l'opérateur d'appel de fonction.

160
anon

C’est l’exemple le plus simple auquel je puisse penser qui évoque 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 séparées count_apples et count_oranges.

66
John McFarlane

Une autre application sont les listes intrusives. Le type d'élément peut indiquer à la liste quels sont ses pointeurs next/prev. La liste n'utilise donc pas de noms codés en dur mais peut toujours utiliser les 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);
}
52

Vous pouvez accéder ultérieurement à ce membre, sur any 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é.
On l'utilise rarement, j'en ai eu besoin peut-être une ou deux fois par an.

Utiliser normalement une interface (c'est-à-dire une classe de base pure en C++) constitue le meilleur choix de conception.

33
peterchen

Voici un exemple concret sur lequel je travaille actuellement, à partir de systèmes de traitement/de 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 fassiez dans un vecteur:

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

Supposons maintenant que vous souhaitiez calculer une fonction (par exemple la moyenne) d'une des variables sur une plage d'échantillons et que vous souhaitiez factoriser ce calcul de moyenne dans une fonction. Le pointeur sur membre facilite les choses:

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 plus concise des fonctions de modèle

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 conséquences sur les performances

Vous devez noter, comme je l’ai bientôt découvert, que le code ci-dessus a de graves conséquences sur les performances. En résumé, si vous calculez une statistique résumée sur une série temporelle, ou si vous calculez une FFT, etc., vous devez alors stocker les valeurs de chaque variable de manière contiguë en mémoire. Sinon, une itération sur la série provoquera une perte de mémoire cache pour chaque valeur extraite.

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 Sample remplira 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 sera jeté, et la prochaine itération entraînera un autre manque de cache, un accès mémoire, etc.

Mieux vaut le faire:

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;

Désormais, lorsque la première valeur x est chargée à partir de la mémoire, les trois suivantes sont é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 itérations suivantes.

L'algorithme ci-dessus peut être amélioré un peu plus loin en utilisant des instructions SIMD sur des architectures SSE2 par exemple. 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 ultérieures SSE).

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

30
Tom

IBM a encore de la documentation sur son utilisation. 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 font référence, alors:

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

Cela semble un peu obscur, mais une des applications possibles est si vous essayez d'écrire du code pour désérialiser des données génériques dans de nombreux types d'objet, et que votre code doit gérer des types d'objet dont il ignore tout 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 offrent 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.

24
AHelps

Il permet de lier des variables et des fonctions membres de manière uniforme. Voici un exemple avec votre classe de voiture. Une utilisation plus courante serait de relier std::pair::first et ::second lors de l'utilisation d'algorithmes STL et de 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;
}
18
Alex B

Vous pouvez utiliser un tableau de pointeurs sur des données de membres (homogènes) pour activer une double interface nommée membre (c'est-à-dire x.data) et tableau-subscript (c'est-à-dire 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;
}
8
Functastic

L’une des méthodes que j’ai utilisées est que 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 à passer continuellement par une instruction if, c.-à-d.

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;
};

Évidemment, cela n’est utile que si vous sentez que le code est suffisamment martelé pour que l’instruction if ralentisse, par exemple. 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 les situations où elle n’a aucune utilité pratique, mais c’est tout simplement mon opinion.

2
Troubadour

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

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 4ème édition

0
Arijit Dey

Voici un exemple où un pointeur sur 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
prestokeys

Je pense que vous ne voudriez le faire que si les données de membre étaient assez volumineuses (par exemple, un objet d'une autre classe assez lourde), et 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
Andrew Jaffe

Juste pour ajouter quelques cas d'utilisation de la réponse de @ anon & @ Oktalist, voici un excellent matériel de lecture sur le pointeur vers la fonction membre et le pointeur vers les données membres . http: //www.cs.wustl .edu/~ schmidt/PDF/C++ - ptmf4.pdf

0
Dragonly

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, disons que vous avez un groupe 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 chargiez les données de sources distinctes, mais que les données soient 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 de readValues() renverra un conteneur avec à l'unisson "input-a" et "input-b"; toutes les clés seront présentes, et les foos avec soit a ou b ou les deux.

0
inetknght