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++98Member 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:
| Operator | Form | Operands |
|---|---|---|
. | Direct member selection | object or reference on the left |
-> | Pointer member selection | pointer on the left |
.* | Pointer-to-member via object | object/reference on the left, pointer-to-member on the right |
->* | Pointer-to-member via pointer | pointer 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 ->
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.
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 pointerThe 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
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.
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.
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.
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.
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:
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:
#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 implicitlyreference/language/const-correctnessβ howconstqualifiers interact with member accessreference/language/autoβ type deduction with pointer-to-member andstd::invoke