Hi everyone ✋
In the previous post we met the this pointer ─ the hidden pointer that lets an object refer to itself. We pushed our house picture nicely along: an object is a house, and this is the card by the door holding its own address.
Today we take that one curious step further and ask ─ if an object can point to itself, can we make a pointer to a single member? Not a member of one particular object, but the idea of that member across every object of the class? 🤔
If we haven’t read the earlier posts in this series, it’s worth going through them first ─ today leans on the arrow operator and this pointer posts especially, and a little of function pointers too.
Today’s topic is pointers to members ─ easily the most exotic pointer in this whole series. They come with strange-looking syntax (Class::*) and two brand-new operators (.* and ->*), and the first time you see them, they can look like someone leaned on the keyboard 😅. But the idea underneath is genuinely elegant, and once it clicks, a whole category of clever, table-driven C++ opens up.
Let’s take a deep dive 🦴
The key idea ─ a pointer that isn’t an address 🤔
Every pointer so far has held an address ─ the location of one specific thing in memory. A pointer to a member is different, and this is the whole trick to understanding it ─ it doesn’t point at any particular object at all.
Instead, it names which member ─ in the abstract. Think of int Student::* as saying “some int member of Student, but I haven’t said which student.” On its own it’s not a place in memory ─ it’s more like an offset, a “which slot in the class” marker. It only becomes a real, reachable member when we combine it with an actual object.
That’s a genuinely different idea from every other pointer, so let’s give it a picture before any code.
My little reference ─ the label on the blueprint 📐
Let’s keep our houses. Every object is a house, and a whole class is the blueprint all those houses were built from ─ same layout, same rooms, over and over.
A normal pointer is the address of one specific house. A pointer to a member is something else entirely ─ it’s a label on the blueprint. Picture the architect’s drawing with an arrow pointing at one room and the word “kitchen.” That label doesn’t refer to any single house’s kitchen ─ it refers to the kitchen slot in the design, wherever that design is built.
By itself, the label “kitchen” isn’t a place ─ we can’t cook in a blueprint 😄. But hand that label a specific house, and it instantly resolves to that house’s kitchen. That handing-over is exactly what the new operators .* and ->* do ─ “take this blueprint label, apply it to this actual house, and give me the real room.”
And it works for fixtures too, not just rooms ─ a label pointing at “the doorbell” is a pointer to a member function. Apply it to a house and it rings that house’s bell.
Keep that blueprint label in mind ─ every bit of odd syntax below is just “which label, applied to which house.” 😉
Pointing at a data member 🏷️
Let’s start with data. To make a pointer to a data member, we name the member with its class in front ─ &Student::marks ─ and store it in a matching type, int Student::* (read it as “pointer to an int member of Student“).
#include <iostream>
#include <string>
using namespace std;
struct Student
{
string name;
int marks;
};
int main()
{
// a label pointing at the 'marks' slot of ANY Student
int Student::*pMarks = &Student::marks;
Student a{ "Ada", 100 };
Student b{ "Bob", 82 };
// apply the label to a specific object with .*
cout << a.*pMarks << endl; // Ada's marks
cout << b.*pMarks << endl; // Bob's marks
// if we hold a pointer to the object, use ->*
Student *ptr = &a;
cout << ptr->*pMarks << endl;
return 0;
}
The output will be ─
100 82 100
Look at what pMarks did ─ one label reached the marks of two different students. a.*pMarks says “the marks slot, applied to house a.” And when we held a pointer to the object instead, we swapped .* for ->*, mirroring exactly the . vs -> rule from the arrow post 🥳.
One crucial detail ⚠️ ─ notice we wrote &Student::marks, not &a.marks. Those are completely different things. &a.marks is a normal int* ─ the address of one specific student’s marks. &Student::marks is the blueprint label ─ an int Student::* that works on any student. The class-qualified form is what makes a member pointer a member pointer.
Pointing at a member function 🔔
The same idea extends to member functions, and the type just grows to describe the function’s shape. A pointer to a Student member function taking no arguments and returning void is written void (Student::*)().
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string name;
int marks;
void display() { cout << name << " scored " << marks << endl; }
};
int main()
{
// a label pointing at the 'display' action of the blueprint
void (Student::*pShow)() = &Student::display;
Student a{ "Ada", 100 };
Student *ptr = &a;
(a.*pShow)(); // ring THIS house's doorbell ─ note the parentheses
(ptr->*pShow)(); // same, through a pointer
return 0;
}
The output will be ─
Ada scored 100 Ada scored 100
Those parentheses around a.*pShow are not optional ⚠️. Just like the (*ptr).member situation from the arrow post, precedence is the culprit ─ the function-call () binds tighter than .*, so without the parentheses the compiler misreads the whole thing. GCC is refreshingly direct about it ─
error: must use '.*' or '->*' to call pointer-to-member function ...
So whenever you call through a member function pointer, wrap the .*/->* part in parentheses first ─ (object.*funcPtr)(args) and (objectPtr->*funcPtr)(args).
A real problem ─ choosing a field at runtime 🎛️
Here’s where member pointers stop being a curiosity and start being genuinely useful. Because a member pointer picks which member without touching which object, we can pass it around like data ─ and let the caller decide which field to work on, at runtime.
Suppose we want one function that prints any numeric field of a whole class of students ─ marks today, ages tomorrow ─ without writing a separate function for each.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Student
{
string name;
int marks;
int age;
};
// print one chosen int field for every student ─ the caller picks WHICH
void printField(const vector<Student>& students, int Student::*field)
{
for (const Student& s : students)
cout << s.*field << " ";
cout << endl;
}
int main()
{
vector<Student> cls = {
{ "Ada", 100, 20 },
{ "Bob", 82, 22 },
{ "Cid", 91, 19 }
};
printField(cls, &Student::marks); // pick the marks field
printField(cls, &Student::age); // pick the age field
return 0;
}
The output will be ─
100 82 91 20 22 19
The same printField produced two completely different reports, and the only thing that changed was the label we handed it ─ &Student::marks versus &Student::age. This is the seed of real, powerful machinery ─ sorting a table by a column chosen at runtime, serialising selected fields, or building generic “get me field X from every row” tools. That’s member pointers earning their keep 🥳.
A real problem ─ sorting by any field 🔀
The field selector reads a chosen column; the natural next step is to sort by a chosen column. This is the pattern behind every table that reorders when a column header is clicked ─ and a single member pointer expresses it beautifully. We hand std::sort a comparator that compares whichever field we were given.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
struct Student
{
string name;
int marks;
int age;
};
// sort the students by whichever int field the caller chooses
void sortBy(vector<Student>& students, int Student::*field)
{
sort(students.begin(), students.end(),
[field](const Student& a, const Student& b)
{
return a.*field < b.*field; // compare the chosen field
});
}
int main()
{
vector<Student> cls = {
{ "Ada", 100, 20 },
{ "Bob", 82, 22 },
{ "Cid", 91, 19 }
};
sortBy(cls, &Student::marks); // order by marks
for (auto& s : cls) cout << s.name << "(" << s.marks << ") ";
cout << endl;
sortBy(cls, &Student::age); // order by age
for (auto& s : cls) cout << s.name << "(" << s.age << ") ";
cout << endl;
return 0;
}
The output will be ─
Bob(82) Cid(91) Ada(100) Cid(19) Ada(20) Bob(22)
One sortBy, two orderings ─ by marks, then by age ─ and again the only thing that changed was the label passed in. The lambda captures field and reaches it on each object with a.*field. This is precisely how generic table and grid components let a user sort by any column without the component knowing a single field name in advance 🥳.
A dispatch table of actions 🎚️
Member function pointers unlock the same trick for behaviour ─ we can build a table of operations and run them in sequence, exactly like the jump tables that function pointers gave us, but tied to an object.
#include <iostream>
using namespace std;
class Calculator
{
int value = 0;
public:
void add() { value += 5; }
void subtract() { value -= 2; }
void show() { cout << "value = " << value << endl; }
};
int main()
{
// a script of operations to apply, in order
void (Calculator::*ops[])() = {
&Calculator::add,
&Calculator::add,
&Calculator::subtract,
&Calculator::show
};
Calculator c;
for (auto op : ops)
(c.*op)(); // apply each label to our calculator
return 0;
}
The output will be ─
value = 8
We stored four member-function labels in an array and applied each to c in turn ─ add, add, subtract, show. This is the backbone of command menus, state machines, and interpreters, where a lookup table maps inputs to the member functions that handle them. No giant if/else ladder ─ just a table of labels 🎛️.
Member functions with arguments ⚙️
Our function-pointer examples so far took no arguments, but the type simply grows to match whatever the function needs. A pointer to a Calculator method taking an int and returning an int is written int (Calculator::*)(int) ─ and we can even re-aim it at any method of the same shape.
#include <iostream>
using namespace std;
class Calculator
{
int value = 10;
public:
int addTo(int n) { return value + n; }
int multiplyBy(int n) { return value * n; }
};
int main()
{
Calculator c;
// a label for "some int-taking, int-returning method of Calculator"
int (Calculator::*op)(int) = &Calculator::addTo;
cout << (c.*op)(5) << endl; // 10 + 5
op = &Calculator::multiplyBy; // re-aim at a different method
cout << (c.*op)(5) << endl; // 10 * 5
return 0;
}
The output will be ─
15 50
Notice op swapped from addTo to multiplyBy without touching c at all ─ both methods share the shape int(int), so the same member pointer fits either. The argument goes after the parenthesised call part, exactly as with a normal call ─ (c.*op)(5).
One more lovely detail ⚠️ ─ member function pointers respect virtual dispatch. If we point at a virtual function through a base class and apply it to a derived object, the derived version still runs. The label says “the doorbell,” and each house rings its own bell ─ polymorphism keeps working right through a member pointer.
The modern way ─ std::invoke and std::mem_fn 🧰
The raw .* and ->* syntax is important to understand, but modern C++ gives us friendlier wrappers built right on top of member pointers. std::invoke (from <functional>, C++17) takes a member pointer and an object and just does the right thing ─
#include <iostream>
#include <string>
#include <functional>
using namespace std;
struct Student
{
string name;
int marks;
void display() { cout << name << " " << marks << endl; }
};
int main()
{
Student a{ "Ada", 100 };
invoke(&Student::display, a); // calls a.display()
cout << invoke(&Student::marks, a) << endl; // reads a.marks
return 0;
}
The output will be ─
Ada 100 100
std::invoke figures out whether it’s a data member or a function, and whether it has an object or a pointer, so we don’t juggle .* and ->* ourselves. Its cousin std::mem_fn wraps a member function into a callable we can hand straight to STL algorithms. These are exactly the tools that libraries lean on ─ and if we’ve used frameworks with property systems, serialization, or signal/slot wiring, member pointers were quietly doing the work underneath 🧵.
Gotchas to keep us safe ⚠️
Let’s gather every tripwire in one place ─
- A member pointer is not an address. It names which member, not where ─ think “offset” or “blueprint label.” It’s useless until combined with an object.
- Use
&Class::member, never&object.member. The first is a member pointer (int Student::*); the second is a plain pointer (int*) to one object’s field. Totally different types. .*with an object,->*with a pointer to an object ─ the same split as.and->.- Parenthesise before calling a member function pointer. Write
(obj.*fp)(args), because()binds tighter than.*. - The class must match. A
Studentmember pointer only works onStudentobjects ─ we can’t point at a member of one class and apply it to another. - The whole signature must match for functions. A pointer of type
int (Calculator::*)(int)fits anyCalculatormethod shapedint(int), but not one with different arguments or return type. - Member function pointers respect virtual dispatch. Through a base-class member pointer, a derived override still runs on a derived object.
- Prefer
std::invoke/std::mem_fnfor real code. They wrap the raw operators and remove most of the chances to slip.
Keep these in mind and the exotic syntax stops being scary 🥳
So what should we remember? 🤔
Let’s wrap up the key takeaways ─
- A pointer to a member is a pointer that names which member of a class, in the abstract ─ it holds no object and is not a memory address.
- Picture the blueprint label 📐 ─ the class is a blueprint, an object is a built house, and a member pointer is a label like “the kitchen” that resolves to a real room only when applied to a specific house.
- Data member pointer: declared
int Student::*, taken with&Student::marks, and used witha.*p(object) orptr->*p(pointer to object). - Member function pointer: declared
void (Student::*)()(orint (Calculator::*)(int)with arguments), taken with&Student::display, and called with(a.*fp)()or(ptr->*fp)()─ parentheses are mandatory, and any arguments go after them. They also honour virtual dispatch. - Always use the class-qualified form
&Class::member;&object.memberis an ordinary pointer to one object’s field, a different type entirely. - Their real power is choosing a member at runtime ─ one
printFieldthat prints any chosen field, asortBythat orders a table by any column, or a dispatch table of member functions for menus and state machines. - Modern C++ wraps all this in
std::invokeandstd::mem_fn, which pick the right access for us ─ and frameworks use member pointers under the hood for properties, serialization, and signals.
Once this clicks, Class::* stops looking like keyboard soup and reads like exactly what it is ─ “a label naming a member of this blueprint, waiting for a house to be applied to.” 👀✨
Congratulations 🥳🥳🥳
We’ve now met even the most exotic pointer in C++ ─ the pointer to a member ─ and seen the elegant, table-driven code it makes possible.
In the next post we’ll turn to the dark side of pointers ─ dangling and wild pointers ─ the pointers that lead to garbage, crashes, and the most notorious bugs in all of C++. We’ll learn where they come from, how to spot them, and how to keep our pointers safe 😉
Happy Coding 💻 🎵