Zeiger auf Klassendatenelement ":: *"


242

Ich bin auf dieses seltsame Code-Snippet gestoßen, das sich gut kompilieren lässt:

class Car
{
    public:
    int speed;
};

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

Warum hat C ++ diesen Zeiger auf ein nicht statisches Datenelement einer Klasse? Was nützt dieser seltsame Zeiger in echtem Code?


Hier ist, wo ich es gefunden habe, verwirrt mich auch ... aber macht jetzt Sinn: stackoverflow.com/a/982941/211160
HostileFork sagt, vertraue SE

Antworten:


188

Es ist ein "Zeiger auf Mitglied" - der folgende Code veranschaulicht seine Verwendung:

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

Was , warum würden Sie wollen, das zu tun, auch sie gibt Ihnen eine weitere Dereferenzierungsebene , die einige knifflige Probleme lösen kann. Aber um ehrlich zu sein, musste ich sie nie in meinem eigenen Code verwenden.

Bearbeiten: Ich kann mir keine überzeugende Verwendung für Zeiger auf Mitgliedsdaten vorstellen. Zeiger auf Mitgliedsfunktionen können in steckbaren Architekturen verwendet werden, aber das Erstellen eines Beispiels auf kleinem Raum besiegt mich erneut. Das Folgende ist mein bester (ungetesteter) Versuch - eine Apply-Funktion, die einige Vor- und Nachbearbeitungen durchführt, bevor eine vom Benutzer ausgewählte Member-Funktion auf ein Objekt angewendet wird:

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

Die Klammern c->*funcsind erforderlich, da der ->*Operator eine niedrigere Priorität hat als der Funktionsaufrufoperator.


3
Können Sie ein Beispiel für eine schwierige Situation zeigen, in der dies nützlich ist? Vielen Dank.
Ashwin Nanjappa

Ich habe ein Beispiel für die Verwendung von Pointer-to-Member in einer Traits-Klasse in einer anderen SO-Antwort .
Mike DeSimone

Ein Beispiel ist das Schreiben einer Klasse vom Typ "Rückruf" für ein ereignisbasiertes System. Das UI-Ereignisabonnementsystem von CEGUI verwendet beispielsweise einen Rückruf mit Vorlagen, in dem ein Zeiger auf eine Mitgliedsfunktion Ihrer Wahl gespeichert ist, sodass Sie eine Methode zur Behandlung des Ereignisses angeben können.
Benji XVI

2
Es ist ein ziemlich cooles Beispiel für Zeiger-zu - Daten -Mitglied Verwendung in einer Template - Funktion in diesem Code
alveko

3
Ich habe kürzlich Zeiger auf Datenelemente im Serialisierungsframework verwendet. Das statische Marshaller-Objekt wurde mit einer Liste von Wrappern initialisiert, die einen Zeiger auf serialisierbare Datenelemente enthalten. Ein früher Prototyp dieses Codes.
Alexey Biryukov

79

Dies ist das einfachste Beispiel, das ich mir vorstellen kann und das die seltenen Fälle vermittelt, in denen diese Funktion relevant ist:

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

Hier ist der an count_fruit übergebene Zeiger zu beachten. Dies erspart Ihnen das Schreiben separater Funktionen count_apples und count_oranges.


3
Sollte es nicht sein &bowls.applesund &bowls.oranges? &bowl::applesund &bowl::orangeszeigt auf nichts.
Dan Nissenbaum

19
&bowl::applesund &bowl::orangeszeigen Sie nicht auf Mitglieder eines Objekts ; Sie zeigen auf Mitglieder einer Klasse . Sie müssen mit einem Zeiger auf ein tatsächliches Objekt kombiniert werden, bevor sie auf etwas zeigen. Diese Kombination wird mit dem ->*Bediener erreicht.
John McFarlane

58

Eine weitere Anwendung sind aufdringliche Listen. Der Elementtyp kann der Liste mitteilen, wie die nächsten / vorherigen Zeiger lauten. Die Liste verwendet also keine fest codierten Namen, kann jedoch vorhandene Zeiger verwenden:

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

Wenn dies wirklich eine verknüpfte Liste ist, möchten Sie nicht so etwas: void add (E * e) {e -> * next_ptr = head; Kopf = e; } ??
Eeeeaaii

4
@eee Ich empfehle Ihnen, über Referenzparameter zu lesen. Was ich getan habe, entspricht im Grunde dem, was Sie getan haben.
Johannes Schaub - litb

+1 für Ihr Codebeispiel, aber ich habe keine Notwendigkeit für die Verwendung von Pointer-to-Member gesehen, irgendein anderes Beispiel?
Alcott

3
@Alcott: Sie können es auf andere verknüpfte Listen-ähnliche Strukturen anwenden, bei denen der nächste Zeiger nicht benannt ist next.
icktoofay

41

Hier ist ein reales Beispiel aus Signalverarbeitungs- / Steuerungssystemen, an dem ich gerade arbeite:

Angenommen, Sie haben eine Struktur, die die Daten darstellt, die Sie sammeln:

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

Angenommen, Sie füllen sie in einen Vektor:

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

Angenommen, Sie möchten eine Funktion (z. B. den Mittelwert) einer der Variablen über einen Bereich von Stichproben berechnen und diese Mittelwertberechnung in eine Funktion einbeziehen. Der Zeiger auf das Mitglied macht es einfach:

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

Hinweis Bearbeitet am 05.08.2016 für einen präziseren Ansatz mit Vorlagenfunktionen

Und natürlich können Sie eine Vorlage erstellen, um einen Mittelwert für jeden Forward-Iterator und jeden Werttyp zu berechnen, der die Addition mit sich selbst und die Division durch size_t unterstützt:

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

BEARBEITEN - Der obige Code hat Auswirkungen auf die Leistung

Wie ich bald herausfand, sollten Sie beachten, dass der obige Code einige schwerwiegende Auswirkungen auf die Leistung hat. Die Zusammenfassung lautet: Wenn Sie eine Zusammenfassungsstatistik für eine Zeitreihe oder eine FFT usw. berechnen, sollten Sie die Werte für jede Variable zusammenhängend im Speicher speichern. Andernfalls führt das Durchlaufen der Serie zu einem Cache-Fehler für jeden abgerufenen Wert.

Betrachten Sie die Leistung dieses Codes:

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;

Auf vielen Architekturen eine Instanz von Sample füllt eine eine Cache-Zeile. Bei jeder Iteration der Schleife wird ein Sample aus dem Speicher in den Cache gezogen. 4 Bytes von der Cache-Zeile werden verwendet und der Rest wird weggeworfen, und die nächste Iteration führt zu einem weiteren Cache-Fehler, Speicherzugriff und so weiter.

Viel besser, um dies zu tun:

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;

Wenn nun der erste x-Wert aus dem Speicher geladen wird, werden auch die nächsten drei in den Cache geladen (vorausgesetzt, die Ausrichtung ist angemessen), sodass für die nächsten drei Iterationen keine Werte geladen werden müssen.

Der obige Algorithmus kann durch die Verwendung von SIMD-Anweisungen auf z. B. SSE2-Architekturen etwas weiter verbessert werden. Diese funktionieren jedoch viel besser, wenn alle Werte im Speicher zusammenhängend sind und Sie mit einem einzigen Befehl vier Samples zusammen laden können (mehr in späteren SSE-Versionen).

YMMV - Entwerfen Sie Ihre Datenstrukturen entsprechend Ihrem Algorithmus.


Das ist ausgezeichnet. Ich bin dabei, etwas sehr Ähnliches zu implementieren, und jetzt muss ich die seltsame Syntax nicht mehr herausfinden! Vielen Dank!
Nicu Stiurca

Dies ist die beste Antwort. Der double Sample::*Teil ist der Schlüssel!
Eyal

37

Sie können später dieses Mitglied Zugriff auf jeden Fall:

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

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

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

  return 0;
}

Beachten Sie, dass Sie eine Instanz benötigen, um sie aufzurufen, damit sie nicht wie ein Delegat funktioniert.
Es wird selten verwendet, ich habe es in all meinen Jahren vielleicht ein- oder zweimal gebraucht.

Normalerweise ist die Verwendung einer Schnittstelle (dh einer reinen Basisklasse in C ++) die bessere Wahl für das Design.


Aber das ist doch nur schlechte Praxis? sollte etwas wie youcar.setspeed (mycar.getpspeed) tun
thecoshman

9
@thecoshman: hängt ganz davon ab - das Verstecken von Datenelementen hinter set / get-Methoden ist keine Kapselung und lediglich ein Versuch der Milchmädchen, die Schnittstelle zu abstrahieren. In vielen Szenarien ist eine "Denormalisierung" für öffentliche Mitglieder eine vernünftige Wahl. Aber diese Diskussion überschreitet wahrscheinlich die Grenzen der Kommentarfunktionalität.
Peterchen

4
+1 für den Hinweis, wenn ich richtig verstehe, dass dies ein Zeiger auf ein Mitglied einer Instanz ist und kein Zeiger auf einen bestimmten Wert einer Instanz, der Teil, den ich vollständig vermisst habe.
Johnbakers

@Fellowshee Du verstehst das richtig :) (betonte das in der Antwort).
Peterchen

26

IBM verfügt über weitere Dokumentationen zur Verwendung. Kurz gesagt, Sie verwenden den Zeiger als Versatz in der Klasse. Sie können diese Zeiger nicht außer der Klasse verwenden, auf die sie sich beziehen.

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

Es scheint ein wenig dunkel zu sein, aber eine mögliche Anwendung ist, wenn Sie versuchen, Code zum Deserialisieren generischer Daten in viele verschiedene Objekttypen zu schreiben, und Ihr Code Objekttypen verarbeiten muss, von denen er absolut nichts weiß (z. B. Ihr Code ist in einer Bibliothek, und die Objekte, in die Sie deserialisieren, wurden von einem Benutzer Ihrer Bibliothek erstellt). Die Elementzeiger bieten Ihnen eine allgemeine, halb lesbare Möglichkeit, auf die einzelnen Datenelement-Offsets zu verweisen, ohne auf typenlose void * -Tricks zurückgreifen zu müssen, wie Sie es für C-Strukturen tun könnten.


Könnten Sie ein Code-Snippet-Beispiel teilen, in dem dieses Konstrukt nützlich ist? Vielen Dank.
Ashwin Nanjappa

2
Ich mache derzeit eine Menge davon, weil ich einige DCOM-Arbeiten erledige und verwaltete Ressourcenklassen verwende, bei denen vor jedem Aufruf ein wenig Arbeit geleistet wird, und die Verwendung von Datenelementen für die interne Darstellung zum Senden an com sowie das Erstellen von Vorlagen viel macht Kesselplattencode viel kleiner
Dan

19

Es ermöglicht die einheitliche Bindung von Mitgliedsvariablen und -funktionen. Das Folgende ist ein Beispiel für Ihre Fahrzeugklasse. Eine häufigere Verwendung wäre verbindlich std::pair::firstund ::secondbei Verwendung in STL-Algorithmen und Boost auf einer Karte.

#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

Sie können ein Array von Zeigern auf (homogene) Mitgliedsdaten verwenden, um eine doppelte Schnittstelle mit benannten Mitgliedern (iexdata) und Array-tiefgestellten (dh x [idx]) zu aktivieren.

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

Ich habe dies häufiger mit einer anonymen Vereinigung einschließlich eines Array-Felds v [3] implementiert gesehen, da dies eine Indirektion vermeidet, aber dennoch klug und möglicherweise nützlich für nicht zusammenhängende Felder ist.
Dwayne Robinson

2
@DwayneRobinson, aber die Verwendung eines unionWortspiels auf diese Weise ist vom Standard nicht zulässig, da es zahlreiche Formen undefinierten Verhaltens hervorruft ... während diese Antwort in Ordnung ist.
underscore_d

Das ist ein gutes Beispiel, aber Operator [] kann ohne Zeiger auf Komponente neu geschrieben werden: Das float *component[] = { &x, &y, &z }; return *component[idx];heißt, der Zeiger auf Komponente scheint keinen anderen Zweck als die Verschleierung zu erfüllen.
tobi_s

2

Eine Möglichkeit, die ich verwendet habe, besteht darin, zwei Implementierungen zu haben, wie etwas in einer Klasse ausgeführt werden soll, und ich möchte zur Laufzeit eine auswählen, ohne ständig eine if-Anweisung durchlaufen zu müssen, d. H.

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

Offensichtlich ist dies nur dann praktisch nützlich, wenn Sie das Gefühl haben, dass der Code so stark gehämmert wird, dass die if-Anweisung die Ausführung verlangsamt, z. tief im Bauch eines intensiven Algorithmus irgendwo. Ich denke immer noch, dass es eleganter ist als die if-Aussage, selbst in Situationen, in denen es keinen praktischen Nutzen hat, aber das ist nur meine Meinung.


Grundsätzlich können Sie dasselbe mit der abstrakten Algorithmund zwei abgeleiteten Klassen erreichen, z . B. AlgorithmAund AlgorithmB. In einem solchen Fall sind beide Algorithmen gut voneinander getrennt und müssen unabhängig voneinander getestet werden.
Shycha

2

Zeiger auf Klassen sind keine echten Zeiger; Eine Klasse ist ein logisches Konstrukt und hat keine physische Existenz im Speicher. Wenn Sie jedoch einen Zeiger auf ein Mitglied einer Klasse erstellen, wird ein Objekt in der Klasse des Mitglieds versetzt, in dem sich das Mitglied befindet. Dies gibt eine wichtige Schlussfolgerung: Da statische Elemente keinem Objekt zugeordnet sind, kann ein Zeiger auf ein Element NICHT auf ein statisches Element (Daten oder Funktionen) verweisen. Beachten Sie Folgendes:

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

Quelle: Die vollständige Referenz C ++ - Herbert Schildt 4. Auflage


0

Ich denke, Sie möchten dies nur tun, wenn die Mitgliedsdaten ziemlich groß waren (z. B. ein Objekt einer anderen ziemlich umfangreichen Klasse) und Sie eine externe Routine haben, die nur mit Verweisen auf Objekte dieser Klasse funktioniert. Sie möchten das Mitgliedsobjekt nicht kopieren, damit Sie es weitergeben können.


0

Hier ist ein Beispiel, in dem ein Zeiger auf Datenelemente nützlich sein könnte:

#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

Angenommen, Sie haben eine Struktur. Innerhalb dieser Struktur befinden sich * irgendeine Art von Name * zwei Variablen des gleichen Typs, aber mit unterschiedlicher Bedeutung

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

Okay, jetzt nehmen wir an, Sie haben ein paar foos in einem Container:

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

Angenommen, Sie laden die Daten aus separaten Quellen, aber die Daten werden auf dieselbe Weise dargestellt (z. B. benötigen Sie dieselbe Analysemethode).

Sie könnten so etwas tun:

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

Zu diesem Zeitpunkt gibt der Aufruf readValues()einen Container mit einer Übereinstimmung von "input-a" und "input-b" zurück. Alle Schlüssel sind vorhanden, und Foos mit haben entweder a oder b oder beides.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.