Skip to content
C++
Language
Intermediate

Member Access

The operators and rules governing access to data members and member functions of class and struct types, including pointer-to-member syntax.

Member Accesssince C++98

Member access refers to the suite of operators and access-control rules that govern how data members and member functions of a class or struct type are reached from an object, pointer, or pointer-to-member.

Overview

C++ provides four built-in operators for member access:

OperatorFormOperands
.Direct member selectionobject or reference on the left
->Pointer member selectionpointer on the left
.*Pointer-to-member via objectobject/reference on the left, pointer-to-member on the right
->*Pointer-to-member via pointerpointer on the left, pointer-to-member on the right

The . and .* operators cannot be overloaded. The -> and ->* operators can be overloaded, a property exploited by every smart pointer type in the standard library.

Access to any member is also governed by an access specifier: public, protected, or private. With struct, the default access for members and base-class inheritance is public; with class, the default is private. This is the only semantic difference between struct and class in C++.

Syntax

Direct access: . and ->

cpp
struct Point { double x, y; };

Point  p{1.0, 2.0};
Point* pp = &p;

double a = p.x;        // dot: object
double b = pp->x;      // arrow: pointer
double c = (*pp).x;    // equivalent to pp->x

-> is definitionally equivalent to (*lhs).rhs; the compiler may optimise them identically, but the pointer dereference (*pp) is explicit with the dot form.

Pointer-to-member: .* and ->*

A pointer-to-member is a typed offset into a class, not a raw address. It encodes both the containing class and the member's type.

cpp
struct Widget {
    int value;
    void print() const;
};

// Pointer to data member
int Widget::*pm = &Widget::value;

Widget  w{42};
Widget* wp = &w;

w.*pm  = 10;     // via object
wp->*pm = 20;    // via pointer

// Pointer to member function
void (Widget::*pf)() const = &Widget::print;

(w.*pf)();       // call via object β€” outer parens are required
(wp->*pf)();     // call via pointer

The outer parentheses around (w.*pf)() are mandatory: .* and ->* bind more loosely than (), so without them the expression would be parsed as w.*(pf()), which is ill-formed.

Access specifiers

cpp
class Account {
public:
    void deposit(double amount);   // accessible everywhere
    double balance() const;

protected:
    double ledger_balance_;        // accessible in Account and derived classes

private:
    int account_id_;               // accessible only within Account
    double raw_balance_;
};

The protected level exists specifically for inheritance: a derived class can read and write protected members of its base but cannot reach private members. Friend declarations bypass all access restrictions for a named class or function.

Examples

Overloading -> for a smart-pointer-like type

-> must return either a raw pointer or an object that itself defines operator->. The chain terminates when a raw pointer is reached.

cpp
template <typename T>
class ScopedPtr {
    T* ptr_;
public:
    explicit ScopedPtr(T* p) : ptr_(p) {}
    ~ScopedPtr() { delete ptr_; }

    T* operator->() const { return ptr_; }
    T& operator*()  const { return *ptr_; }
};

struct Config { int timeout; std::string host; };

ScopedPtr<Config> cfg{new Config{30, "localhost"}};
cfg->timeout = 60;          // invokes operator->(), then accesses timeout
std::string h = cfg->host;

Overloading ->*

Overloading ->* is rare but arises when building proxy or delegate types that must support the full pointer-to-member protocol.

cpp
template <typename T>
class Proxy {
    T* obj_;
public:
    explicit Proxy(T* o) : obj_(o) {}
    T* operator->() const { return obj_; }

    // Return a callable bound to the member pointer
    template <typename R, typename... Args>
    auto operator->*(R (T::*mf)(Args...)) const {
        return [this, mf](Args... args) -> R {
            return (obj_->*mf)(std::forward<Args>(args)...);
        };
    }
};

Since C++11, lambdas make ->* overloads far more ergonomic than the manual functor approach required before.

Pointer-to-member as a dispatch table

A classic application: storing an array of member-function pointers to avoid a chain of if/switch branches.

cpp
class StateMachine {
    using Handler = void (StateMachine::*)(int);

    void on_idle(int event);
    void on_running(int event);
    void on_error(int event);

    static constexpr Handler table_[] = {
        &StateMachine::on_idle,
        &StateMachine::on_running,
        &StateMachine::on_error,
    };
    int state_ = 0;

public:
    void dispatch(int event) {
        (this->*table_[state_])(event);
    }
};

this->*table_[state_] retrieves the pointer-to-member and binds it to this in one expression.

Member access in templates with dependent names

When accessing a member through a template type parameter, the compiler cannot know during parsing whether a dependent name is a type, a value, or a template. The typename and template disambiguators are required.

cpp
template <typename Container>
void process(Container& c) {
    // Without 'typename', the compiler might parse '::value_type' as a non-type
    typename Container::value_type first = c.front();

    // 'template' disambiguates a member template following a dependent name
    auto it = c.template begin<int>();
}

This was a C++98 rule codified more clearly in C++11 and further clarified in C++17, where many previously required typename annotations in non-deduced contexts became optional in C++20.

Best Practices

Prefer -> over (*ptr).. The arrow form removes one level of parentheses and makes pointer intent explicit without adding verbosity.

Use typedef or using to alias pointer-to-member types. The raw declaration syntax is notoriously hard to read, and wrapping it in an alias pays dividends immediately:

cpp
using Handler = void (Widget::*)(int);
Handler h = &Widget::process;

Keep access specifiers coarse-grained. Group all public members together, then protected, then private. Mixing specifiers within a section forces readers to track which rule applies to each member.

Prefer std::invoke (C++17) over direct .*/->* when calling through a member-function pointer at a call site. std::invoke handles regular functions, lambdas, and pointer-to-member uniformly, which simplifies generic code:

cpp
#include <functional>  // C++17

auto call = [](auto&& obj, auto&& mem, auto&&... args) {
    return std::invoke(mem, obj, std::forward<decltype(args)>(args)...);
};

Common Pitfalls

Forgetting parentheses around .*/->* calls. (obj.*pmf)(args) is valid; obj.*pmf(args) attempts to call pmf as a regular function first, which will not compile if pmf is a pointer-to-member type.

Taking the address of a virtual function and treating it as a final dispatch. &Base::virtual_fn gives a pointer to that slot in the class, not to a specific override. The correct override is still selected at runtime through the vtable when you call through the pointer β€” this is expected behaviour, but confuses engineers who expect the address to freeze the dispatch.

Accessing a member through -> on a null pointer. This is undefined behaviour even if the accessed member is not the result of a dereference that would fault. Some compilers appear to tolerate ptr->static_member through null, but the standard provides no guarantee.

Mismatching the class in a pointer-to-member with the object type. A Derived::* pointer-to-member can be used with a Base only if the member was inherited β€” the compiler enforces this, but the error messages for pointer-to-member type mismatches are often cryptic.

See Also

  • reference/language/special-member-functions β€” constructors, destructors, and other compiler-generated members accessed implicitly
  • reference/language/const-correctness β€” how const qualifiers interact with member access
  • reference/language/auto β€” type deduction with pointer-to-member and std::invoke