Pointers to Members

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 Student member pointer only works on Student objects ─ 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 any Calculator method shaped int(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_fn for 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 with a.*p (object) or ptr->*p (pointer to object).
  • Member function pointer: declared void (Student::*)() (or int (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.member is an ordinary pointer to one object’s field, a different type entirely.
  • Their real power is choosing a member at runtime ─ one printField that prints any chosen field, a sortBy that 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::invoke and std::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 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *