Wskaźnik do elementu danych klasy „:: *”


243

Natknąłem się na ten dziwny fragment kodu, który dobrze się kompiluje:

class Car
{
    public:
    int speed;
};

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

Dlaczego C ++ ma ten wskaźnik do niestatycznego elementu danych klasy? Jaki jest pożytek z tego dziwnego wskaźnika w prawdziwym kodzie?


Oto, gdzie go znalazłem, też mnie zdezorientowałem ... ale ma to teraz sens: stackoverflow.com/a/982941/211160
HostileFork mówi: nie ufaj

Odpowiedzi:


190

Jest to „wskaźnik do elementu członkowskiego” - poniższy kod ilustruje jego użycie:

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

Co do tego , dlaczego chcesz to zrobić, to daje kolejny poziom pośrednictwa, który może rozwiązać niektóre trudne problemy. Ale szczerze mówiąc, nigdy nie musiałem używać ich we własnym kodzie.

Edycja: Nie mogę od razu przekonać się o przekonującym zastosowaniu wskaźników do danych członka. Wskaźnik do funkcji składowych może być używany w architekturach wtykowych, ale po raz kolejny stworzenie przykładu w małej przestrzeni mnie pokonuje. Oto moja najlepsza (nie przetestowana) próba - funkcja Apply, która wykonałaby pewne wstępne i końcowe przetwarzanie przed zastosowaniem funkcji członkowskiej wybranej przez użytkownika do obiektu:

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

Nawiasy wokół c->*funcsą konieczne, ponieważ ->*operator ma niższy priorytet niż operator wywołania funkcji.


3
Czy możesz podać przykład trudnej sytuacji, w której jest to przydatne? Dzięki.
Ashwin Nanjappa

Mam przykład użycia wskaźnika do członka w klasie Traits w innej odpowiedzi SO .
Mike DeSimone,

Przykładem jest napisanie klasy typu „callback” dla niektórych systemów opartych na zdarzeniach. Na przykład system subskrypcji zdarzeń interfejsu użytkownika CEGUI pobiera szablon wywołania zwrotnego, który przechowuje wskaźnik do wybranej funkcji członka, aby można było określić metodę obsługi zdarzenia.
Benji XVI,

2
Jest całkiem fajny przykład użycia wskaźnika do danych w członkach w funkcji szablonu w tym kodzie
alveko

3
Ostatnio użyłem wskaźników do członków danych w ramach serializacji. Statyczny obiekt marshaller został zainicjowany za pomocą listy opakowań zawierających wskaźnik do szeregowalnych elementów danych. Wczesny prototyp tego kodu.
Alexey Biryukov

79

To najprostszy przykład, jaki mogę wymyślić, który przedstawia rzadkie przypadki, w których ta funkcja jest istotna:

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

Należy tutaj zwrócić uwagę na wskaźnik przekazywany do count_fruit. Dzięki temu nie musisz pisać osobnych funkcji count_apples i count_oranges.


3
Czy nie powinno być &bowls.applesi &bowls.oranges? &bowl::applesi &bowl::orangesniczego nie wskazuje.
Dan Nissenbaum

19
&bowl::applesi &bowl::orangesnie wskazują na członków obiektu ; wskazują na członków klasy . Muszą być połączone ze wskaźnikiem do rzeczywistego obiektu, zanim wskażą na coś. Ta kombinacja została osiągnięta z ->*operatorem.
John McFarlane

58

Kolejną aplikacją są natrętne listy. Typ elementu może powiedzieć liście, jakie są jego następne / poprzednie wskaźniki. Tak więc lista nie używa zakodowanych nazw, ale nadal może korzystać z istniejących wskaźników:

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

Jeśli jest to naprawdę połączona lista, nie chciałbyś czegoś takiego: void add (E * e) {e -> * next_ptr = head; głowa = e; } ??
eeeeaaii,

4
@eee Polecam przeczytać o parametrach odniesienia. To, co zrobiłem, jest w zasadzie równoznaczne z tym, co zrobiłeś.
Johannes Schaub - litb

+1 w twoim przykładzie kodu, ale nie widziałem żadnej potrzeby użycia wskaźnika do elementu, żadnego innego przykładu?
Alcott,

3
@Alcott: Możesz zastosować go do innych struktur podobnych do listy połączonej, w których następny wskaźnik nie jest nazwany next.
icktoofay

41

Oto rzeczywisty przykład, nad którym teraz pracuję, z systemów przetwarzania / kontroli sygnałów:

Załóżmy, że masz strukturę reprezentującą gromadzone dane:

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

Załóżmy teraz, że umieścisz je w wektorze:

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

Załóżmy teraz, że chcesz obliczyć jakąś funkcję (powiedzmy średnią) jednej ze zmiennych w zakresie próbek i chcesz uwzględnić to obliczenie średnie w funkcji. Wskaźnik do członka ułatwia:

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

Uwaga Zmodyfikowano 2016/08/05, aby uzyskać bardziej zwięzłe podejście do funkcji szablonu

I oczywiście możesz szablonować go, aby obliczyć średnią dla dowolnego iteratora do przodu i dowolnego typu wartości, który obsługuje dodawanie z samym sobą i dzielenie według 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);

EDYCJA - Powyższy kod ma wpływ na wydajność

Należy zauważyć, jak wkrótce odkryłem, że powyższy kod ma poważne konsekwencje dla wydajności. Podsumowanie jest takie, że jeśli obliczasz statystyki podsumowujące na szeregach czasowych lub obliczasz FFT itp., Powinieneś przechowywać wartości dla każdej zmiennej w sposób ciągły w pamięci. W przeciwnym razie iteracja po serii spowoduje brak pamięci podręcznej dla każdej odzyskanej wartości.

Rozważ wydajność tego kodu:

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;

W wielu architekturach jedno wystąpienie Samplewypełnia wiersz pamięci podręcznej. Tak więc przy każdej iteracji pętli jedna próbka zostanie wyciągnięta z pamięci do pamięci podręcznej. Zostaną wykorzystane 4 bajty z linii pamięci podręcznej, a resztę wyrzucone, a następna iteracja spowoduje kolejne pominięcie pamięci podręcznej, dostęp do pamięci i tak dalej.

Znacznie lepiej to zrobić:

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;

Teraz, gdy pierwsza wartość x jest ładowana z pamięci, kolejne trzy również zostaną załadowane do pamięci podręcznej (zakładając odpowiednie wyrównanie), co oznacza, że ​​nie potrzebujesz żadnych wartości załadowanych dla następnych trzech iteracji.

Powyższy algorytm można nieco ulepszyć, stosując instrukcje SIMD np. W architekturach SSE2. Jednak działają one znacznie lepiej, jeśli wszystkie wartości są ciągłe w pamięci i można użyć jednej instrukcji, aby załadować razem cztery próbki (więcej w późniejszych wersjach SSE).

YMMV - zaprojektuj struktury danych zgodnie z algorytmem.


To jest wspaniałe. Zaraz zaimplementuję coś bardzo podobnego, a teraz nie muszę wymyślać dziwnej składni! Dzięki!
Nicu Stiurca

To najlepsza odpowiedź. Ta double Sample::*część jest kluczowa!
Eyal

37

Możesz później uzyskać dostęp do tego członka w dowolnej instancji:

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

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

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

  return 0;
}

Pamiętaj, że potrzebujesz instancji, aby ją wywołać, więc nie działa ona jak delegat.
Jest używany rzadko, potrzebowałem go może raz lub dwa razy przez wszystkie lata.

Zwykle lepszym wyborem jest użycie interfejsu (tj. Czystej klasy bazowej w C ++).


Ale z pewnością jest to po prostu zła praktyka? powinien zrobić coś takiego jak youcar.setspeed (mycar.getpspeed)
thecoshman

9
@ thecoshman: całkowicie zależy - ukrywanie członków danych za metodami set / get nie jest hermetyzacją, a jedynie próbą abstrakcji interfejsu służącą mleczu. W wielu scenariuszach „denormalizacja” członków publicznych jest rozsądnym wyborem. Ale ta dyskusja prawdopodobnie wykracza poza zakres funkcji komentowania.
peterchen

4
+1 za wskazanie, o ile dobrze rozumiem, że jest to wskaźnik do członka dowolnej instancji, a nie wskaźnik do określonej wartości jednej instancji, której brakowało mi całkowicie.
johnbakers

@Fellowshee Rozumiesz poprawnie :) (podkreślił to w odpowiedzi).
peterchen

26

IBM ma więcej dokumentacji na temat korzystania z tego. W skrócie, używasz wskaźnika jako przesunięcia w klasie. Nie możesz używać tych wskaźników oprócz klasy, do której się odnoszą, więc:

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

Wydaje się to trochę niejasne, ale jedną z możliwych aplikacji jest próba napisania kodu do deserializacji danych ogólnych na wiele różnych typów obiektów, a twój kod musi obsługiwać typy obiektów, o których absolutnie nic nie wie (na przykład twój kod jest w bibliotece, a obiekty, na które deserializujesz, zostały utworzone przez użytkownika Twojej biblioteki). Wskaźniki składowe dają ogólny, częściowo czytelny sposób odwoływania się do odsunięć poszczególnych składowych danych, bez konieczności uciekania się do typless void *, które można zastosować do struktur typu C.


Czy możesz udostępnić przykładowy fragment kodu, w którym ta konstrukcja jest przydatna? Dzięki.
Ashwin Nanjappa

2
Obecnie dużo tego robię, ponieważ wykonuję trochę pracy w DCOM i korzystam z zarządzanych klas zasobów, co wiąże się z odrobiną pracy przed każdym wywołaniem, a także z wykorzystaniem elementów danych do wewnętrznej reprezentacji do wysyłania do com, a także tworzenia szablonów, robi dużo kocioł kod płyta znacznie mniejszy
Dan

19

Umożliwia wiązanie zmiennych i funkcji członka w jednolity sposób. Oto przykład z klasą samochodu. Bardziej powszechne użycie byłoby wiążące std::pair::firsti ::secondprzy użyciu w algorytmach STL i doładowania na mapie.

#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

Możesz użyć tablicy wskaźnika do (jednorodnych) danych członka, aby włączyć podwójny interfejs o nazwie nazwany (iexdata) i indeks-tablica (tj. 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;
}

Częściej widziałem to zaimplementowane przy użyciu anonimowego związku, w tym pola tablicowego v [3], ponieważ pozwala to uniknąć pośredniego, ale mimo to sprytnego i potencjalnie przydatnego w przypadku pól niesąsiadujących.
Dwayne Robinson

2
@DwayneRobinson, ale użycie takiego uniontypu pisowni w ten sposób nie jest dozwolone przez standard, ponieważ wywołuje wiele form nieokreślonego zachowania ... podczas gdy ta odpowiedź jest dobra.
underscore_d

To fajny przykład, ale operator [] można przepisać bez wskaźnika do komponentu: float *component[] = { &x, &y, &z }; return *component[idx];tzn. Wskaźnik do komponentu wydaje się służyć jedynie celowi zaciemniającemu.
tobi_s

2

Jednym ze sposobów jest to, że mam dwie implementacje, jak zrobić coś w klasie i chcę wybrać jedną w czasie wykonywania bez ciągłego przechodzenia przez instrukcję if, tj.

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

Oczywiście jest to praktycznie przydatne tylko wtedy, gdy uważasz, że kod jest wystarczająco wbijany, aby instrukcja if spowalniała wykonywanie zadań, np. gdzieś głęboko w żołądku jakiegoś intensywnego algorytmu. Nadal uważam, że jest bardziej elegancki niż stwierdzenie if, nawet w sytuacjach, w których nie ma praktycznego zastosowania, ale to tylko mój pomysł.


Zasadniczo, można osiągnąć to samo z abstrakcyjne Algorithmi dwóch klas pochodnych, np, AlgorithmAa AlgorithmB. W takim przypadku oba algorytmy są dobrze rozdzielone i zapewnione jest ich niezależne testowanie.
shycha

2

Wskaźniki do klas nie są rzeczywistymi wskaźnikami; klasa jest konstrukcją logiczną i nie ma fizycznej pamięci w pamięci, jednak kiedy konstruujesz wskaźnik do członka klasy, daje on przesunięcie do obiektu klasy członka, w którym można go znaleźć; Daje to ważny wniosek: ponieważ elementy statyczne nie są powiązane z żadnym obiektem, więc wskaźnik do elementu NIE MOŻE wskazywać na element statyczny (dane lub funkcje), należy wziąć pod uwagę następujące kwestie :

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

Źródło: The Complete Reference C ++ - Herbert Schildt 4th Edition


0

Myślę, że powinieneś to zrobić tylko wtedy, gdy dane członka były dość duże (np. Obiekt innej dość potężnej klasy) i masz jakąś zewnętrzną procedurę, która działa tylko na odwołania do obiektów tej klasy. Nie chcesz kopiować obiektu członka, więc możesz go przekazać.


0

Oto przykład, w którym wskaźnik do elementów danych może być przydatny:

#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

Załóżmy, że masz strukturę. Wewnątrz tej struktury znajduje się * jakaś nazwa * dwie zmienne tego samego typu, ale o innym znaczeniu

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

OK, powiedzmy teraz, że masz kilka foos w pojemniku:

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

Okej, teraz załóżmy, że ładujesz dane z oddzielnych źródeł, ale dane są prezentowane w ten sam sposób (np. Potrzebujesz tej samej metody analizy).

Możesz zrobić coś takiego:

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

W tym momencie wywołanie readValues()zwróci kontener z zestawieniem „input-a” i „input-b”; wszystkie klucze będą obecne, a foos z mają albo a albo b albo oba.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.