类数据成员指针"::*"

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

class Car
{
public:
int speed;
};


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

为什么 c++有这个指针指向类的非静态数据成员吗?什么是使用这个奇怪的指针在真实的代码?

199232 次浏览

你可以稍后在任何实例中访问这个成员:

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++中的纯基类)是更好的设计选择。

IBM有更多关于如何使用它的文档。简单地说,您使用指针作为类的偏移量。你不能在它们所指向的类之外使用这些指针,所以:

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

这看起来有点晦涩,但一个可能的应用是,如果您试图编写代码将泛型数据反序列化为许多不同的对象类型,并且您的代码需要处理它完全不知道的对象类型(例如,您的代码在一个库中,而您反序列化的对象是由库的用户创建的)。成员指针为您提供了一种通用的、半易读的方式来引用各个数据成员偏移量,而不必像使用C结构体那样使用无类型的void *技巧。

它是一个“指向成员的指针”——下面的代码说明了它的用法:

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

至于为什么,你会想这样做,它给了你另一个间接的层次,可以解决一些棘手的问题。但说实话,我从未在自己的代码中使用过它们。

编辑:我不能马上想到一个令人信服的使用指针成员数据。指向成员函数的指针可以在可插拔的体系结构中使用,但是在这么小的空间里生成一个例子再次让我感到挫败。以下是我最好的(未经测试的)try -一个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周围的括号是必要的,因为->*操作符的优先级低于函数调用操作符。

我认为,只有当成员数据相当大(例如,另一个相当庞大的类的对象),并且您有一些外部例程,只对该类的对象引用起作用时,才会想要这样做。你不想复制成员对象,所以这让你可以传递它。

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

它使得以统一的方式绑定成员变量和函数成为可能。下面是Car类的示例。更常见的用法是在STL算法和Boost中使用映射时绑定std::pair::first::second

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

另一个应用是侵入式列表。元素类型可以告诉列表它的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);
}

您可以使用指向(同构)成员数据的指针数组来启用双重命名成员(即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;
}

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

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

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 -设计适合你的算法的数据结构。

这是我能想到的最简单的例子,它传达了这个特性很少相关的情况:

#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函数。

下面是一个例子,其中指向数据成员的指针可能很有用:

#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
}
假设你有一个结构。在那个结构里面 *某种名字 *两个相同类型但含义不同的变量

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

好,现在假设你在一个容器中有一堆__abc:

// 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。

只是添加一些用例@anon的&@Oktalist的回答,这里有一个关于指向成员函数的指针和指向成员数据的很棒的阅读材料。

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

指向类的指针不是真正的指针;类是一个逻辑构造,在内存中没有物理存在,然而,当你构造一个指向类成员的指针时,它会给出一个指向该成员所在类的对象的偏移量;这给出了一个重要的结论:由于静态成员不与任何对象相关联,因此指向成员的指针不能指向静态成员(数据或函数)

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第四版

一个指向成员的指针的真实例子可以是std::shared_ptr的更窄的混叠构造函数:

template <typename T>
template <typename U>
shared_ptr<T>::shared_ptr(const shared_ptr<U>, T U::*member);

构造函数有什么用

假设你有一个结构体foo:

struct foo {
int ival;
float fval;
};

如果你给了一个foo对象一个shared_ptr对象,你可以使用构造函数将shared_ptr对象检索到它的成员ival或fval:

auto foo_shared = std::make_shared<foo>();
auto ival_shared = std::shared_ptr<int>(foo_shared, &foo::ival);

如果想将指针foo_shared->ival传递给某个需要shared_ptr的函数,这将非常有用

https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr

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

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偏移是如何工作的?

使用指向成员的指针,我们可以编写这样的泛型代码

template<typename T, typename U>
struct alpha{
T U::*p_some_member;
};


struct beta{
int foo;
};


int main()
{


beta b{};


alpha<int, beta> a{&beta::foo};


b.*(a.p_some_member) = 4;


return 0;
}

我喜欢*&操作符:

struct X
{
int a {0};
int *ptr {NULL};


int &fa() { return a; }
int *&fptr() { return ptr; }
};


int main(void)
{
X x;
int X::*p1 = &X::a;     // pointer-to-member 'int X::a'. Type of p1 = 'int X::*'
x.*p1 = 10;


int *X::*p2 = &X::ptr;  // pointer-to-member-pointer 'int *X::ptr'. Type of p2 = 'int *X::*'
x.*p2 = nullptr;
X *xx;
xx->*p2 = nullptr;


int& (X::*p3)() = X::fa; // pointer-to-member-function 'X::fa'. Type of p3 = 'int &(X::*)()'
(x.*p3)() = 20;
(xx->*p3)() = 30;


int *&(X::*p4)() = X::fptr;  // pointer-to-member-function 'X::fptr'. Type of p4 = 'int *&(X::*)()'
(x.*p4)() = nullptr;
(xx->*p4)() = nullptr;
}

事实上,只要成员是公共的或静态的,所有都是真的