指向类数据成员“ :: *”的指针


242

我遇到了一个编译正常的奇怪代码段:

class Car
{
    public:
    int speed;
};

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

为什么 C ++具有指向类的非静态数据成员的指针?什么是使用真正的代码,这个奇怪的指针?


我在这里找到它的地方,也让我感到困惑...但是现在很有意义:stackoverflow.com/a/982941/211160
HostileFork说不要相信2012年

Answers:


188

它是“成员的指针”-以下代码说明了其用法:

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

至于您为什么要这样做,那么它为您提供了可以解决一些棘手问题的另一种间接方式。但老实说,我从来没有在自己的代码中使用它们。

编辑:我不能认为有说服力的使用指向成员数据的指针。指向成员函数的指针可用于可插拔体系结构,但再次在较小的空间中产生示例使我感到失望。以下是我最好的(未尝试的)尝试-一个Apply函数,该函数在将用户选择的成员函数应用于对象之前会进行一些前置和后置处理:

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
您能否举例说明一个有用的棘手情况?谢谢。
Ashwin Nanjappa,09年

我有一个在另一个SO答案的Traits类中使用指向成员的指针的示例。
Mike DeSimone

一个示例是为某些基于事件的系统编写“回调”类型的类。例如,CEGUI的UI事件订阅系统采用模板回调,该回调存储指向所选成员函数的指针,以便您可以指定处理事件的方法。
Benji XVI

2
此代码中的模板函数有一个很酷的指针指向数据成员的用法示例
alveko 2013年

3
我最近在序列化框架中使用了指向数据成员的指针。使用包含指向可序列化数据成员的指针的包装器列表初始化静态编组器对象。此代码的早期原型。
Alexey Biryukov 2015年

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没有指向任何东西。
Dan Nissenbaum 2014年

19
&bowl::apples并且&bowl::oranges不要指向一个对象的成员; 他们指向班上的成员。它们需要与指向实际对象的指针结合在一起,然后才能指向某物。这种结合是由->*操作员实现的。
约翰·麦克法兰2014年

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

如果这确实是一个链表,那么您就不需要这样:void add(E * e){e-> * next_ptr = head; 头= e; } ??
eeeeaaii 2011年

4
@eee我建议您阅读参考参数。我所做的基本上等于您所做的。
Johannes Schaub-litb 2011年

您的代码示例为+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);

编辑-上面的代码对性能有影响

您应该注意到,正如我很快发现的那样,以上代码对性能有严重影响。摘要是,如果您要计算某个时间序列的摘要统计信息,或计算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值时,接下来的三个值也将被加载到缓存中(假设合适的对齐方式),这意味着在接下来的三个迭代中不需要加载任何值。

通过在例如SSE2体系结构上使用SIMD指令,可以在某种程度上进一步改善上述算法。但是,如果值在内存中都是连续的,则这些方法更好地工作,并且您可以使用一条指令将四个样本一起加载(在以后的SSE版本中更多)。

YMMV-设计您的数据结构以适合您的算法。


太好了 我将要实现非常相似的东西,现在我不必弄清楚奇怪的语法了!谢谢!
Nicu Stiurca 2013年

这是最好的答案。该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 2010年

9
@thecoshman:完全取决于-将数据成员隐藏在set / get方法后面并不是封装,而只是挤奶女工试图进行接口抽象。在许多情况下,对公共成员进行“非规范化”是一个合理的选择。但是该讨论可能超出了评论功能的范围。
peterchen

4
+1指出(如果我正确理解的话)这是一个指向任何实例成员的指针,而不是指向一个实例的特定值的指针,而这正是我完全缺少的部分。
johnbakers

@Fellowshee您确实理解正确:)(在答案中强调了这一点)。
彼得

26

IBM有更多有关如何使用它的文档。简而言之,您将指针用作类的偏移量。您不能在引用的类之外​​使用这些指针,因此:

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

似乎有些晦涩,但是一个可能的应用是如果您试图编写将通用数据反序列化为许多不同对象类型的代码,并且您的代码需要处理完全不了解的对象类型(例如,您的代码是在库中,反序列化的对象是由库的用户创建的)。成员指针为您提供了一种通用的,半清晰的方式来引用各个数据成员的偏移量,而不必诉诸无类型的void *技巧,可能会使用C结构。


您可以分享一个使用此结构的代码片段示例吗?谢谢。
Ashwin Nanjappa,09年

2
由于要进行一些DCOM工作并使用托管资源类,因此我目前正在做大量此类工作,这涉及在每次调用之前做一些工作,并使用数据成员进行内部表示以发送给com,再加上模板,使得很多锅炉板代码很多

19

这样就可以以统一的方式绑定成员变量和函数。以下是您的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;
}

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]的匿名联合来实现此功能,因为它避免了间接访问,但仍然很聪明,并且可能对非连续字段有用。
Dwayne Robinson

2
@DwayneRobinson但union标准不允许使用以这种方式键入双关语,因为它会调用多种形式的未定义行为……而这个答案是可以的。
underscore_d

那是一个很好的例子,但是operator []可以在没有指向组件的指针的情况下进行重写: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语句更优雅,但这只是我的选择。


基本上,您可以使用abstract Algorithm和两个派生类(例如AlgorithmA和)实现相同的目的AlgorithmB。在这种情况下,两种算法都可以很好地分开,并确保独立进行测试。
shycha '19

2

指向类的指针不是真正的指针。一个类是一个逻辑结构,并且在内存中没有物理存在,但是,当您构造一个指向该类成员的指针时,它会为该成员类的对象提供一个偏移量,在该对象中可以找到该成员;这给出了一个重要的结论:由于静态成员没有与任何对象关联,因此指向成员的指针不能指向静态成员(数据或函数),无论如何 请考虑以下事项:

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 ++-Herbert Schildt第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;
};

好的,现在假设您foo在容器中有一堆s:

// 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或两者都有。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.