Вказівник на даних даних класу ":: *"


243

Я натрапив на цей дивний фрагмент коду, який чудово компілює:

class Car
{
    public:
    int speed;
};

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

Чому C ++ має цей покажчик на нестатичного члена даних класу? Яке використання цього дивного вказівника у реальному коді?


Ось де я знайшов його, бентежив мене теж ... але має сенс зараз: stackoverflow.com/a/982941/211160
HostileFork говорить не довіряє SE

Відповіді:


190

Це "вказівник на члена" - наступний код ілюструє його використання:

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

Щодо того, чому б ви хотіли це зробити, це дає вам ще один рівень непрямості, який може вирішити деякі складні проблеми. Але якщо чесно, мені ніколи не довелося використовувати їх у власному коді.

Редагувати: Я не можу подумати про переконливе використання покажчиків на дані членів. Функції вказівника на член можна використовувати в підключаються архітектурах, але вкотре, створюючи приклад у невеликому просторі, мене перемагає. Наступна моя найкраща (неперевірена) спроба - функція застосувати, яка б виконала деяку попередню та пост-обробку, перш ніж застосувати обрану користувачем функцію до об’єкта:

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

Дужки навколо c->*funcпотрібні, оскільки ->*оператор має нижчий пріоритет, ніж оператор виклику функцій.


3
Чи можете ви показати приклад хитрої ситуації, коли це корисно? Дякую.
Ешвін Нанджаппа

У мене є приклад використання вказівника на члена в класі Traits в іншій відповіді SO .
Майк Десімоне,

Прикладом є написання класу типу "зворотний виклик" для деякої системи на основі подій. Наприклад, система підписки на події UI CEGUI приймає шаблонний зворотний виклик, який зберігає вказівник на вибрану функцію члена, щоб ви могли вказати метод для обробки події.
Бенджі XVI

2
Є досить класний приклад використання вказівника на дані- члена в функції шаблону в цьому коді
alveko

3
Нещодавно я використовував покажчики на членів даних у рамках серіалізації. Статичний об'єкт маршалера був ініціалізований списком обгортки, що містить вказівник на серіалізаційні члени даних. Ранній прототип цього коду.
Олексій Бірюков

79

Це найпростіший приклад, який я можу придумати, який передає рідкісні випадки, коли ця функція доречна:

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

Тут слід зазначити, що вказівник передається count_fruit. Це заощаджує вам необхідність писати окремі функції count_apples та count_oranges.


3
Чи не повинно бути &bowls.applesі &bowls.oranges? &bowl::applesі &bowl::orangesне вказує ні на що.
Дан Ніссенбаум

19
&bowl::applesі &bowl::orangesне вказують на членів об'єкта ; вони вказують на членів класу . Їх потрібно поєднувати з вказівником на фактичний об’єкт, перш ніж вони щось вказують. Це поєднання досягається з ->*оператором.
Джон Макфарлейн

58

Ще однією програмою є нав'язливі списки. Тип елемента може повідомити списку, що таке його наступний / попередній покажчики. Таким чином, у списку не використовуються жорстко зашифровані імена, але все ще можна використовувати наявні вказівники:

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

Якщо це справді пов'язаний список, ви не хочете щось подібне: недійсне додавання (E * e) {e -> * next_ptr = head; голова = е; } ??
eeeeaaii

4
@eee Я рекомендую вам ознайомитися з еталонними параметрами. Те, що я зробив, в основному еквівалентне тому, що ви зробили.
Йоханнес Шауб - ліб

+1 для вашого прикладу коду, але я не бачив жодної необхідності у використанні покажчика на учасника, будь-якого іншого прикладу?
Алькотт

3
@Alcott: Ви можете застосувати його до інших структур, подібних до списку, де наступний вказівник не названий next.
icktoofay

41

Ось приклад із реального світу, над яким я працюю зараз, з систем обробки / управління сигналами:

Припустимо, у вас є структура, яка представляє дані, які ви збираєте:

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

Тепер припустімо, що ви складете їх у вектор:

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

Тепер припустимо, що ви хочете обчислити деяку функцію (скажімо середнє) однієї зі змінних у діапазоні зразків, і ви хочете занести цей середній обчислення у функцію. Вказівник на члена полегшує:

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

Примітка відредаговано 2016/08/05 для більш стислого підходу шаблон-функція

І, звичайно, ви можете шаблонувати його для обчислення середнього значення для будь-якого ітератора вперед та будь-якого типу значення, який підтримує додавання з собою та поділ на 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 - Наведений вище код має наслідки для продуктивності

Вам слід зазначити, як я незабаром виявив, що наведений вище код має серйозні наслідки для продуктивності. Підсумок полягає в тому, що якщо ви обчислюєте підсумкову статистику за часовим рядом або обчислюєте FFT тощо, то слід зберігати значення для кожної змінної безперервно в пам'яті. В іншому випадку повторення серіалу призведе до пропуску кешу для кожного отриманого значення.

Розглянемо ефективність цього коду:

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;

У багатьох архітектурах один екземпляр Sample заповнює рядок кешу. Отже, на кожній ітерації циклу один зразок буде витягнуто з пам'яті в кеш. 4 байти з кеш-лінії будуть використані, а решта викинуті, а наступна ітерація призведе до чергової пропуски кешу, доступу до пам'яті тощо.

Набагато краще це зробити:

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;

Тепер, коли перше значення x завантажено з пам'яті, наступні три також будуть завантажені в кеш (припустимо, що відповідне вирівнювання), тобто для наступних трьох ітерацій не потрібно завантажувати жодних значень.

Наведений вище алгоритм можна дещо вдосконалити за допомогою використання інструкцій SIMD щодо напр., Архітектур SSE2. Однак, вони працюють набагато краще, якщо ці значення є суміжними в пам'яті, і ви можете використовувати одну інструкцію для завантаження чотирьох зразків разом (докладніше в пізніших версіях SSE).

YMMV - спроектуйте свої структури даних відповідно до вашого алгоритму.


Це чудово. Я збираюся реалізувати щось дуже схоже, і тепер мені не доведеться з'ясовувати дивний синтаксис! Дякую!
Nicu Stiurca

Це найкраща відповідь. double Sample::*Частина є ключем!
Eyal

37

Пізніше ви можете отримати доступ до цього учасника в будь-якому випадку:

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

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

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

  return 0;
}

Зауважте, що вам потрібен екземпляр, щоб викликати його, тому він не працює як делегат.
Він використовується рідко, мені це було потрібно один-два рази за всі мої роки.

Зазвичай використання інтерфейсу (тобто чистого базового класу на C ++) є кращим вибором дизайну.


Але напевно це лише погана практика? повинен зробити щось на кшталт youcar.setspeed (mycar.getpspeed)
thecoshman

9
@thecoshman: повністю залежить - приховування членів даних за методами set / get - це не інкапсуляція, а лише спроба доярок на абстракції інтерфейсу. У багатьох сценаріях "денормалізація" для громадських членів є розумним вибором. Але це обговорення, ймовірно, перевищує межі функціональних коментарів.
peterchen

4
+1 для вказівки, якщо я правильно розумію, що це вказівник на члена будь-якої інстанції, а не вказівник на конкретне значення одного екземпляра, що є тією частиною, яку я повністю пропускав.
джонбейкери

@Fellowshee Ви правильно розумієте :) (підкреслив, що у відповіді).
peterchen

26

IBM має ще трохи документації про те, як це використовувати. Якщо коротко, ви використовуєте вказівник як зміщення в класі. Ви не можете використовувати ці вказівники крім класу, на який вони посилаються, тому:

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

Це здається трохи незрозумілим, але одне можливе застосування - якщо ви намагаєтеся написати код для десеріалізації загальних даних у багато різних типів об'єктів, і ваш код повинен обробляти типи об'єктів, про які він абсолютно нічого не знає (наприклад, ваш код: в бібліотеці, а об’єкти, в які ви десеріалізуєте, створені користувачем вашої бібліотеки). Покажчики членів дають вам загальний, напіврозбірливий спосіб посилання на окремі компенсації учасників даних, без необхідності вдаватися до безхарактерних пустот * хитрощів, як ви могли б використовувати структури С.


Чи можете ви поділитися прикладом фрагмента коду, де ця конструкція корисна? Дякую.
Ешвін Нанджаппа

2
В даний час я багато цього роблю завдяки тому, що роблю DCOM роботу і використовую керовані класи ресурсів, що передбачає зробити трохи роботи перед кожним викликом, а також використання членів даних для внутрішнього представлення для відправки на кому, а також шаблонування, робить багато Код пластини котла набагато менший
День

19

Це дає можливість пов'язувати змінні та функції членів однаково. Нижче наводиться приклад з вашим класом автомобілів. Більш поширене використання буде обов'язковим std::pair::firstі ::secondпри використанні в алгоритмах STL та Boost на карті.

#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

Ви можете використовувати масив вказівника на (однорідні) дані члена, щоб увімкнути подвійний, іменований член (iexdata) та масив-підпис (тобто 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;
}

Я частіше бачив, що це реалізується за допомогою анонімного об'єднання, включаючи поле масиву v [3], оскільки це дозволяє уникнути непрямого, але, тим не менш розумного, і може бути корисним для непоміжних полів.
Дуейн Робінсон

2
@DwayneRobinson, але використовувати unionтиповий каламбур таким способом заборонено стандартом, оскільки він посилається на численні форми невизначеної поведінки ... тоді як ця відповідь нормальна.
підкреслюйте_d

Це акуратний приклад, але оператор [] може бути переписаний без вказівника на компонент: float *component[] = { &x, &y, &z }; return *component[idx];Тобто, вказівник на компонент, здається, не виконує жодної мети, окрім обфускування.
tobi_s

2

Один із способів, яким я користувався, це якщо у мене є дві реалізації того, як робити щось у класі, і я хочу вибрати його під час виконання без необхідності постійно переглядати оператор if, тобто

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

Очевидно, це практично практично корисно, якщо ви вважаєте, що код забитий достатньо, щоб оператор if уповільнював дії, наприклад. десь глибоко в кишках якогось інтенсивного алгоритму. Я все ще думаю, що це більш елегантно, ніж твердження if, навіть у ситуаціях, коли воно не має практичного використання, але це лише моя думка.


В основному, ви можете досягти того ж з абстрактним Algorithmта двома похідними класами, наприклад, AlgorithmAта AlgorithmB. У такому випадку обидва алгоритми добре розділені і гарантуються, що вони будуть перевірені незалежно.
шича

2

Покажчики на класи не є реальними покажчиками; клас - це логічна конструкція і не має фізичного існування в пам'яті, однак, коли ви конструюєте вказівник на члена класу, він дає зміщення в об’єкт класу члена, де його можна знайти; Це дає важливий висновок: Оскільки статичні члени не асоціюються з жодним об'єктом, тож вказівник на член CANNOT вказує на статичний член (дані або функції) .

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

Джерело: Повна довідка C ++ - Герберт Шильдт, 4-е видання


0

Я думаю, ви хотіли б це зробити, лише якщо дані членів були досить великими (наприклад, об'єктом іншого досить здоровенного класу), і у вас є зовнішня рутина, яка працює тільки на посилання на об'єкти цього класу. Ви не хочете копіювати об’єкт-член, тому це дозволяє передати його навколо.


0

Ось приклад, коли вказівник на членів даних може бути корисним:

#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

Припустимо, у вас є структура. Всередині цієї структури є * якесь ім’я * дві змінні одного типу, але з різним значенням

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

Гаразд, тепер скажімо, у вас є купа foos в контейнері:

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

Гаразд, тепер припустимо, ви завантажуєте дані з окремих джерел, але дані подаються однаково (наприклад, вам потрібен той же метод розбору).

Ви можете зробити щось подібне:

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

У цей момент виклик readValues()поверне контейнер з унісон "input-a" та "input-b"; всі клавіші будуть присутніми, а foos з a або b або обома.


0

Просто для додання деяких випадків використання для відповіді на @ anon & @ Oktalist, ось чудовий матеріал для читання про вказівник на член-функцію та дані вказівника на члена.

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


посилання загинула. Ось чому відповідей, що стосуються лише посилань, тут не очікується. Принаймні узагальнюйте вміст посилання, інакше ваша відповідь стає недійсною, коли посилання загниває
phuclv
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.