Pointer to members is a long story to tell. First we assume that you've known what a normal pointer is.
Pointer to members suggests that it can point to the specific member of any instance of class. There are two types of pointer to members, first to member variables and second to member functions.
Before that, the variables and functions can be static or non-static. For static ones, it's no other than normal global ones from the program's perspective, e.g. in Linux ELF, static data are stored in .data
directly, where the global variables are also stored. From the angle of the programmers, they are just accessing a special global function / variable as well, just adding some Class::
. So, the pointer to static member variable / function is just the same as the pointer to a normal variable / function.
Now let's talk about the non-static ones. Non-static members should always bind to some specific object, e.g. obj.a
or obj.func()
and Class::a
or Class::func()
is illegal. Then, is it possible to use a pointer to suggest that "I hope to point to a specific member of any instance, and when I want to use it, I will bind an instance"? That's what the pointer to members do.
Wait... you may think: "That bothers! Why can't I just use the .
?". To maintain the consistency, we will go back to this question finally. Now we assume it's useful first, and see what syntax it uses.
class ClassOpTest
{
public:
int nsVar; // non-static variable.
void nsFunc(int){return;} // non-static function.
};
int ClassOpTest::* nsVarPtr = &ClassOpTest::nsVar;
void (ClassOpTest::*nsFuncPtr)(int) = &ClassOpTest::nsFunc;
int main()
{
ClassOpTest object2;
ClassOpTest* object2Ptr = &object2;
object.*nsVarPtr = 1; // equals to object.nsVar = 1;
object2.*nsVarPtr = 2; // equals to object2.nsVar = 2;
object2Ptr->*nsVarPtr = 3; // equals to object2.nsVar = 3;
// Note that these paratheses are necessary, considering the operation order.
// If there are not, nsFuncPtr() will be resolved first rather than object.*nsFuncPtr().
// That is, the compiler will regard the nsFuncPtr as a normal function (pointer)
// rather than pointer to member function, so "obj.*" is just a meaningless mess.
// All in all, no paratheses will cause compilation error.
(object.*nsFuncPtr)(1); // equals to object.nsFunc(1);
(object2Ptr->*nsFuncPtr)(2); // equals to object2.nsFunc(2);
return 0;
}
You may find it's troublesome to write types like this, so you can use deduced type in C++11 as:
using ClassOpTestIntPtr = decltype(&ClassOpTest::nsVar);
using ClassOpTestFuncPtr = decltype(&ClassOpTest::nsFunc);
ClassOpTestIntPtr nsVarPtr = &ClassOpTest::nsVar;
ClassOpTestFuncPtr nsFuncPtr = &ClassOpTest::nsFunc;
Notice that the decltype
doesn't mean it always points to nsVar
or nsFunc
; it means the type same as them.
You may also think .*
or ->*
is oblique(me too!), then you can use std::invoke
in C++17 like this :
std::invoke(nsVarPtr, object2) = 1; // equals to object.*nsVarPtr = 1;
std::invoke(nsVarPtr, &object2) = 2; // equals to object2Ptr->*nsVarPtr = 2;
// both work.
std::invoke(nsFuncPtr, object2, 1); // equals to (object.*nsFunc)(1);
std::invoke(nsFuncPtr, &object2, 2); // equals to (object2Ptr->*nsFunc)(2);
std::invoke
is significantly useful, but that's not the point of the answer. In a nutshell, it will use corresponding operator when the second calling parameter varies.
Finally, why is it useful? In my point of view, that's mostly because the pointer only conveys the type, and the type may infer lots of members. For instance:
struct RGB
{
std::uint8_t r;
std::uint8_t g;
std::uint8_t b;
};
and I hope to blend two std::vector<RGB>
using Intel's SIMD intrinsics. First for r
, that is:
reg1 = _mm_set_epi16(RGBdata1[i + 7].r, RGBdata1[i + 6].r, RGBdata1[i + 5].r,
RGBdata1[i + 4].r, RGBdata1[i + 3].r, RGBdata1[i + 2].r,
RGBdata1[i + 1].r, RGBdata1[i].r);
reg2 = _mm_set_epi16(RGBdata2[i + 7].r, RGBdata2[i + 6].r, RGBdata2[i + 5].r,
RGBdata2[i + 4].r, RGBdata2[i + 3].r, RGBdata2[i + 2].r,
RGBdata2[i + 1].r, RGBdata2[i].r);
reg1 = _mm_mullo_epi16(reg1, alphaReg1);
reg2 = _mm_mullo_epi16(reg2, alphaReg2);
resultReg1 = _mm_add_epi16(reg1, reg2);
// for simplicity, code below omitted; there are also manys operation to get the result.
// ...
// store back
_mm_store_si128((__m128i*)buffer, resultReg1);
for(int k = 0; k < 16; k++)
{
outRGBdata[i + k].r = buffer[k];
}
So what about g
and b
? Oops, okay, you have to paste the code twice. What if you find some bugs and want to change something? You have to paste again for g
and b
. That suffers! If we use pointer to members, then :
using RGBColorPtr = std::uint8_t RGB::*;
void SIMDBlendColor(RGB* begin1, RGB* begin2,
RGB* outBegin, RGBColorPtr color,
__m128i alphaReg1, __m128i alphaReg2)
{
__m128i resultReg1, reg1, reg2;
alignas(16) std::uint8_t buffer[16];
reg1 = _mm_set_epi16((begin1 + 7)->*color, (begin1 + 6)->*color,
(begin1 + 5)->*color, (begin1 + 4)->*color,
(begin1 + 3)->*color, (begin1 + 2)->*color,
(begin1 + 1)->*color, begin1->*color);
reg2 = _mm_set_epi16((begin2 + 7)->*color, (begin2 + 6)->*color,
(begin2 + 5)->*color, (begin2 + 4)->*color,
(begin2 + 3)->*color, (begin2 + 2)->*color,
(begin2 + 1)->*color, begin2->*color);
reg1 = _mm_mullo_epi16(reg1, alphaReg1);
reg2 = _mm_mullo_epi16(reg2, alphaReg2);
resultReg1 = _mm_add_epi16(reg1, reg2);
// ...
_mm_store_si128((__m128i*)buffer, resultReg1);
for(int k = 0; k < 16; k++)
{
(outBegin + k)->*color = buffer[k];
}
return;
}
Then, you can just call like this :
SIMDBlendColor(RGBdata1.data() + i, RGBdata2.data() + i, outRGBdata.data() + i, &RGB::r, alphaReg1, alphaReg2);
SIMDBlendColor(RGBdata1.data() + i, RGBdata2.data() + i, outRGBdata.data() + i, &RGB::g, alphaReg1, alphaReg2);
SIMDBlendColor(RGBdata1.data() + i, RGBdata2.data() + i, outRGBdata.data() + i, &RGB::b, alphaReg1, alphaReg2);
Clean and beautiful!
BTW, I strongly recommend you to check iso-cpp-wiki for more information.