Указатель на элемент данных класса ":: *"

Я наткнулся на этот странный фрагмент кода, который компилируется отлично:

class Car
{
    public:
    int speed;
};

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

Почему имеет ли С++ этот указатель на нестатический член данных класса? Что - использование этого странного указателя в реальном коде?

Ответы

Ответ 1

Это "указатель на член" - следующий код иллюстрирует его использование:

#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 необходимы, поскольку оператор ->* имеет более низкий приоритет, чем оператор вызова функции.

Ответ 2

Это самый простой пример, который я могу представить, который передает редкие случаи, когда эта особенность имеет значение:

#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

Другое приложение - это навязчивые списки. Тип элемента может рассказать о том, каковы его следующие/предыдущие указатели. Таким образом, список не использует жестко заданные имена, но все равно может использовать существующие указатели:

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

Ответ 4

Вы можете позже получить доступ к этому члену, в любом экземпляре:

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

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

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

  return 0;
}

Обратите внимание, что вам нужно, чтобы экземпляр вызывал его, поэтому он не работает, как делегат.
Он используется редко, мне он понадобился один или два раза за все мои годы.

Обычно использование интерфейса (т.е. чистый базовый класс в С++) является лучшим выбором дизайна.

Ответ 5

Здесь реальный пример, над которым я сейчас работаю, от систем обработки/управления сигналами:

Предположим, что у вас есть структура, которая представляет данные, которые вы собираете:

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 - проектируйте свои структуры данных в соответствии с вашим алгоритмом.

Ответ 6

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

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

Кажется немного неясным, но одно возможное приложение - это если вы пытаетесь написать код для десериализации общих данных во множество разных типов объектов, а ваш код должен обрабатывать типы объектов, о которых он абсолютно ничего не знает (например, ваш код находится в библиотеке, а объекты, в которые вы deserialize были созданы пользователем вашей библиотеки). Указатели-члены предоставляют вам общий, полуразборный способ обращения к отдельным смещениям данных, без необходимости прибегать к беспринципным void * трюкам, как вы могли бы использовать для C-структур.

Ответ 7

Это позволяет единообразно связывать переменные-члены и функции. Ниже приведен пример вашего класса Car. Более распространенное использование было бы привязкой 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;
}

Ответ 8

Вы можете использовать массив указателей на (однородные) данные элемента, чтобы включить интерфейс с именем-членом (например, x.data) и массивом-индексом (т.е. 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;
}

Ответ 9

Один из способов, которым я использовал это, - это если у меня есть две реализации того, как делать что-то в классе, и я хочу выбрать его во время выполнения без необходимости постоянно выполнять оператор if.e.

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 даже в ситуациях, когда оно не имеет практического применения, но это просто мое одобрение.

Ответ 10

Я думаю, вы только хотели бы сделать это, если бы данные элемента были довольно большими (например, объект другого довольно здоровенного класса), и у вас есть внешняя подпрограмма, которая работает только с ссылками на объекты этого класса. Вы не хотите копировать объект-член, поэтому это позволяет передать его.

Ответ 11

Вот пример, в котором может быть полезен указатель на данные:

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

Ответ 12

Предположим, что у вас есть структура. Внутри этой структуры * свое имя * две переменные того же типа, но с разным значением

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

Хорошо, теперь скажем, что у вас есть куча foo в контейнере:

// 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, либо оба.

Ответ 13

Просто добавьте некоторые варианты использования для ответов @anon и @Oktalist, вот отличный материал для чтения о функции-указателе-члене и данных-указателях-членах. http://www.cs.wustl.edu/~schmidt/PDF/C++-ptmf4.pdf

Ответ 14

Указатели на классы не являются настоящими указателями; класс является логической конструкцией и не имеет физического существования в памяти, однако, когда вы создаете указатель на член класса, он дает смещение в объект класса-члена, где член может быть найден; Это дает важный вывод: поскольку статические члены не связаны с каким-либо объектом, указатель на член НЕ МОЖЕТ указывать на статический член (данные или функции). Рассмотрим следующее:

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-е издание