我遇到了这个奇怪的代码片段,它编译得很好:

class Car
{
    public:
    int speed;
};

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

为什么c++有这个指针指向类的非静态数据成员?在实际代码中,这个奇怪的指针有什么用呢?


当前回答

下面是我现在正在研究的一个现实世界的例子,来自信号处理/控制系统:

假设你有一些表示你正在收集的数据的结构:

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值从内存中加载时,接下来的三个x值也将加载到缓存中(假设适当的对齐),这意味着您不需要为接下来的三个迭代加载任何值。

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

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

其他回答

我使用它的一种方式是,如果我有两个如何在一个类中做某事的实现,我想在运行时选择一个,而不必连续地通过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语句更优雅,即使在它没有实际用途的情况下,但这只是我的观点。

另一个应用是侵入式列表。元素类型可以告诉列表它的next/prev指针是什么。所以列表不使用硬编码的名称,但仍然可以使用现有的指针:

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

指向成员的指针是c++的类型安全等价于C的offsetof(),它在stddef.h中定义:两者都返回某个字段位于类或结构中的信息。虽然在c++中也可以将offset()用于某些足够简单的类,但在一般情况下,它会失败,尤其是虚拟基类。因此指针成员被添加到标准中。它们还提供了更简单的语法来引用实际字段:

struct C { int a; int b; } c;
int C::* intptr = &C::a;       // or &C::b, depending on the field wanted
c.*intptr += 1;

要比:

struct C { int a; int b; } c;
int intoffset = offsetof(struct C, a);
* (int *) (((char *) (void *) &c) + intoffset) += 1;

至于为什么要使用offsetof()(或指向成员的指针),在stackoverflow的其他地方有很好的答案。这里有一个例子:宏的C偏移是如何工作的?

假设你有一个结构。在那个结构里面 *某种名字 *两个相同类型但含义不同的变量

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”一致的容器;所有的键都将出现,带有a或b或两者都有的foo。

下面是我现在正在研究的一个现实世界的例子,来自信号处理/控制系统:

假设你有一些表示你正在收集的数据的结构:

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值从内存中加载时,接下来的三个x值也将加载到缓存中(假设适当的对齐),这意味着您不需要为接下来的三个迭代加载任何值。

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

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