A class member can be declared with one of three access specifiers [N4950 S14.3.1]:
Access control is enforced at compile time only; it has zero runtime cost. The access specifier Applies to all members declared after it until another access specifier is encountered.
`class` for types that maintain invariants and require encapsulation.A friend declaration grants a function or another class access to private and protected Members of the class that grants friendship. Friendship is not transitive , not inherited , And not symmetric : if class A declares B as a friend, B can access A”s private members, but A Cannot access B’s, and B’s derived classes cannot access A’s private members.
A friend declaration is a declaration that nominates a function or class to be granted access to Non-public members. The key properties are:
Granting is unilateral. The class that contains the friend declaration is the granting class. The nominated entity receives access; no reciprocal access is implied.Not transitive. If A declares B as a friend, and B declares C as a friend, C does not have access to A’s private members.Not inherited. If A declares B as a friend, and C inherits from B``C does not have access to A’s private members.Friendship is not a membership declaration. A friend function is not a member of the class. It does not have a this pointer and is not found by name lookup within the class scope (unless it is also declared as a member). friend Vector operator * ( const Matrix & m , const Vector & v );
Vector ( double x , double y , double z ) {
data_[ 0 ] = x; data_[ 1 ] = y; data_[ 2 ] = z;
double operator [] ( std :: size_t i ) const { return data_[i]; }
friend Vector operator * ( const Matrix & m , const Vector & v );
Matrix ( std :: initializer_list < std :: initializer_list < double >> il ) {
Vector operator * ( const Matrix & m , const Vector & v ) {
for ( std :: size_t i = 0 ; i < 3 ; ++ i) {
for ( std :: size_t j = 0 ; j < 3 ; ++ j) {
sum += m.m_[i][j] * v.data_[j];
Matrix m{{ 1 , 0 , 0 }, { 0 , 1 , 0 }, { 0 , 0 , 1 }};
std :: printf ( " result: %.1f %.1f %.1f \n " , r[ 0 ], r[ 1 ], r[ 2 ]);
By [N4950 S14.3.2], a friend of a class C is a function or class that is granted access to the Non-public members of C. The proof that friendship is not transitive follows from the definition: The access check in [N4950 S14.3] examines whether the entity attempting access is a friend of the Class being accessed. If A grants friendship to BAnd B grants friendship to CThe access Check for C accessing A’s private members examines whether C is a friend of A. Since C is Not declared as a friend of AAccess is denied.
Similarly, inheritance is irrelevant to friendship: [N4950 S14.3] specifies that “a member of a Derived class… Has no special access to members of a base class” except through the normal access Specifiers. Since C inherits from BAnd B is a friend of A``C is not a friend of A by The same argument.
Friendship can be granted to an entire class or to a specific member function of another class:
void inspect ( const Engine & e );
friend class Mechanic ; // entire class is a friend
friend void DiagnosticTool :: inspect ( const Engine & ); // one member
void set_rpm ( int rpm ) { rpm_ = rpm; }
void set_temp ( double t ) { temp_ = t; }
e.rpm_ = 800 ; // OK: Mechanic is a friend
std :: printf ( " Tuned: rpm= %d , temp= %.1f \n " , e.rpm_, e.temp_);
void DiagnosticTool :: inspect ( const Engine & e ) {
std :: printf ( " Inspect: rpm= %d , temp= %.1f \n " , e.rpm_, e.temp_);
When granting friendship to a specific member function, the function must have been declared (but Not necessarily defined) before the friend declaration. This is why DiagnosticTool::inspect is Forward-declared in the example above.
A hidden friend is a friend function defined inside a class body. Unlike a free friend declared Outside, a hidden friend is found by argument-dependent lookup (ADL) only — it is not found by Ordinary unqualified name lookup [N4950 S9.4.1].
friend Metric operator + ( Metric a , Metric b ) {
return Metric{a.value_ + b.value_};
friend Metric operator * ( Metric m , double scale ) {
return Metric{m.value_ * scale};
friend double get_value ( const Metric & m ) {
explicit Metric ( double v ) : value_ (v) {}
auto c = a + b; // OK: found by ADL on Metric
auto d = a * 2.0 ; // OK: found by ADL on Metric
std :: printf ( " %.1f %.1f \n " , get_value (c), get_value (d));
// operator+(a, b); // ERROR: not found by unqualified lookup
Hidden friends are the preferred idiom for defining operators in modern C++ because:
They do not pollute the enclosing namespace. They are only found when the associated class is in scope, preventing unintended overload resolution. They have access to private members without needing a separate friend declaration outside the class. Friendship should be used sparingly. The two most common legitimate use cases are:
Symmetric binary operators : When the left operand does not belong to the class (e.g., ostream& operator<<(ostream&, const T&) or Vector operator*(const Matrix&, const Vector&)).Factory patterns : When a factory function needs access to a private constructor.Internal helpers : When a utility function needs deep access but should not be a member.Access specifiers on base classes control how inherited members are accessible in the derived class. This is distinct from the access specifiers on individual members within a class.
class PubDerived : public Base {
std :: cout << pub << " \n " ; // OK: public, accessible
std :: cout << prot << " \n " ; // OK: protected, accessible in derived
// std::cout << priv << "\n"; // ERROR: private, not accessible
class PrivDerived : private Base {
std :: cout << pub << " \n " ; // OK: still accessible within PrivDerived
std :: cout << prot << " \n " ; // OK: still accessible within PrivDerived
std :: cout << pd.pub << " \n " ; // OK: public inheritance preserves public access
// std::cout << pd.prot << "\n"; // ERROR: protected, not accessible outside
// std::cout << prd.pub << "\n"; // ERROR: private inheritance makes everything private
Base Member Access Public Inheritance Protected Inheritance Private Inheritance publicpublicprotectedprivateprotectedprotectedprotectedprivateprivateinaccessible inaccessible inaccessible
Private inheritance is not an “is-a” relationship — it is an “implemented-in-terms-of” Relationship. It is used when you want to reuse a base class’s implementation without exposing the Base interface to users.
You can restore the access level of inherited members with a using declaration [N4950 S11.4.6]:
class Adapter : private PrivateBase {
using PrivateBase :: public_func; // Restore public access
using PrivateBase :: protected_func; // Also restore protected to public
a. public_func (); // OK: access was restored by using-declaration
a. protected_func (); // OK: access was restored
The using declaration makes the named member accessible with the access level of the using Declaration itself (in this case, public). This is the standard mechanism for selectively exposing Members when using private inheritance.
Template instantiation interacts with access control in specific ways. Access control is checked at The point of instantiation, not at the point of definition. This means a friend of a class can Access private members during template instantiation.
template < typename T > friend void inspect ( T & );
// Access to value_ is checked when T = Secret
// At that point, inspect is a friend of Secret
// This is valid even though value_ is private
std :: cout << " inspecting \n " ;
The Curiously Recurring Template Pattern (CRTP) commonly requires the derived class to access Private members of the base:
template < typename Derived >
void increment () { ++ count_; }
int count () const { return count_; }
class Widget : private Counter < Widget > {
friend class Counter < Widget >;
void click () { increment (); }
int clicks () const { return count (); }
std :: cout << " Clicks: " << w. clicks () << " \n " ; // Output: 2
The final specifier prevents further derivation or overriding. It is enforced at compile time with Zero runtime cost. final can appear in two contexts:
Class final: A class marked final shall not be used as a base class.Member function final: A virtual function marked final shall not be overridden in any derived class. virtual void process () { std :: cout << " Base::process \n " ; }
virtual ~Base () = default ;
class Final : public Base {
void process () final { std :: cout << " Final::process \n " ; }
// class Derived : public Final {}; // ERROR: cannot derive from 'final' class
The final specifier on a virtual function prevents further overriding in derived classes:
class Mid : public Base {
void process () override final { std :: cout << " Mid::process \n " ; }
// class Leaf : public Mid {
// void process() override {} // ERROR: process is final
final enables devirtualization : if the compiler can prove that a virtual call targets a final class or method, it can replace the indirect call with a direct call or even inline the Function. This is because final provides a static guarantee that no further override exists, Eliminating the need for runtime dispatch.
Proof sketch: By [N4950 S11.7.4], a class marked final “shall not appear as a base class.” If the Compiler sees a call obj.f() where obj has static type FinalClass and FinalClass is marked finalThen the dynamic type of obj is necessarily FinalClass (no derived class can exist). Therefore, the virtual dispatch resolves statically to FinalClass::fAnd the compiler can emit a Direct call.
A nested class is a member of its enclosing class [N4950 S13.4.2]. The access rules for nested Classes follow from this membership relationship:
A nested class has access to all members of its enclosing class (including private and protected members). This follows from [N4950 S14.3]: a member function of the nested class is considered a member of the enclosing class for access checking purposes. The enclosing class does not have special access to the nested class’s private members. The nested class’s private members are accessible only to the nested class’s own members and friends. void access_outer ( Outer & o ) {
std :: cout << o.secret_ << " \n " ; // OK: nested class accesses enclosing private
friend class Inner ; // Implicit -- nested classes are implicitly friends of enclosing
// i.inner_secret_; // ERROR: enclosing class cannot access nested's private
i. access_outer ( * this ); // OK
A nested class may declare friends just like any other class. These friends have access only to the Nested class’s members, not to the enclosing class’s members:
friend void inner_friend ( Inner & i );
std :: cout << o.outer_priv << " \n " ; // OK: nested accesses enclosing
void inner_friend ( Outer :: Inner & i ) {
std :: cout << i.inner_priv << " \n " ; // OK: friend of Inner
// i.outer_priv; // ERROR: not a friend of Outer
Access control is fully enforced in constexpr and consteval contexts. A constexpr function Cannot access private members of an unrelated class, even at compile time.
friend int break_in ( const Vault & );
consteval int break_in ( const Vault & v ) {
return v.code_; // OK: friend access, evaluated at compile time
constexpr int result = break_in (Vault{});
static_assert (result == 1337 );
A using declaration (inside a class) affects access, but a using directive (at namespace Scope) does not bypass class access control:
friend void reveal ( const Secret & );
void public_method () const { std :: cout << " public \n " ; }
void reveal ( const Secret & s ) {
std :: cout << s.data_ << " \n " ;
// reveal(s); // OK: reveal is found by ordinary lookup after using-directive
An inline friend function defined inside a class body is subject to the same access rules as any Other friend: it can access all members of the granting class. The inline specifier affects Linkage (multiple definitions are allowed across translation units) but has no effect on access.
Access control is like a building’s security system: public is the lobby — anyone can walk in. protected is the employee area — only employees (derived classes) can enter. private is the CEO’s office — only the class itself can access it. friend is like a visitor pass — it grants selective access to non-employees. The key insight is that access control is a compile-time property, not a runtime one — there’s zero overhead, and it’s enforced entirely by the compiler.
Why it matters: Access control is the primary mechanism for enforcing encapsulation in C++. Without it, any code could modify any member, making it impossible to maintain invariants. The friend mechanism is often overused — prefer public/protected interfaces and only grant friendship when absolutely necessary.
The key insight: Access control is compile-time only — it has zero runtime cost, but it’s not a security boundary. A reinterpret_cast can bypass it, so don’t rely on it for security.
Assuming friendship is transitive or inherited. If A declares B as a friend, and C inherits from B``C does not have access to A’s private members. Each class controls its own friendship independently.Using protected data members. While syntactically legal, protected data members break encapsulation because any derived class can modify them directly without the base class’s knowledge. Prefer protected member functions (getters/setters) or private data with protected accessors.Forgetting that class defaults to private and struct defaults to public. A struct with no access specifier has public members by default, which can accidentally expose implementation details. Always be explicit about access specifiers.Overusing friendship. Every friend declaration creates a tight coupling between two classes. Prefer public interfaces, member functions, or the hidden friend idiom for operators. Reserve friendship for cases where no alternative exists (symmetric operators, factories).Private inheritance confusion. Private inheritance is not a substitute for composition. It inherits the base class’s layout (vtable, sizeof), which increases coupling. Use composition (member variable) unless you specifically need protected member access or virtual function overriding.Friend function name hiding. A friend function defined inside a class body (hidden friend) is not found by unqualified lookup outside of ADL. If you need the function to be callable without ADL, declare it outside the class.Using-declarations and overloads. A using declaration in a derived class makes accessible all overloads of the named member from the base class. If only one overload needs to be exposed, you must use a forwarding function instead, since a using declaration cannot target a single overload.Access control applies uniformly to all member functions, including those defined inside the class Body. A member function defined inside the class body is implicitly inlineBut this does not Affect its access to private members of the same class:
void helper () { std :: cout << " helper: " << secret_ << " \n " ; }
helper (); // OK: member function accesses private member function
std :: cout << " secret: " << secret_ << " \n " ;
// sh.helper(); // ERROR: helper is private
// sh.secret_; // ERROR: secret_ is private
A friend of a class has the same access as a member function. This means a friend function can Access all private and protected members. However, the friend cannot grant its access to third Parties:
friend void inspect ( A & a );
std :: cout << a.data_ << " \n " ; // OK: friend of A
// A separate function that is NOT a friend of A
void external ( A & a , void (* inspector )( A & )) {
inspector (a); // OK: calls the friend function
// a.data_; // ERROR: external is not a friend of A
A lambda defined inside a member function can capture this (or *this) and access private members Through the captured pointer. This is because the lambda’s call operator is conceptually a member of The enclosing scope, and access checking uses the enclosing context:
void add ( int val ) { data_. push_back (val); }
// Lambda captures 'this' (const, since process() is const)
// Can access private data_ through the captured this pointer
auto it = std :: find_if (data_. begin (), data_. end (),
[ this ]( int val ) { return val > threshold (); });
std :: cout << " Found: " << * it << " (threshold= " << threshold () << " ) \n " ;
int threshold () const { return 5 ; }
Protected access has a subtle restriction: a member function of a derived class can access protected members of the base class only through a pointer or reference to the derived class (or a Class derived from it), not through a pointer or reference to the base class directly:
std :: cout << value_ << " \n " ; // OK: implicit this is Derived*
void access_through_derived ( Derived & d ) {
std :: cout << d.value_ << " \n " ; // OK: through Derived&
void access_through_base ( Base & b ) {
// std::cout << b.value_ << "\n"; // ERROR: protected access through Base&
// This is because b could refer to any Base subobject, and the protected
// member might belong to a different Derived object.
// The access rule prevents accessing protected members of sibling objects.
This rule, specified in [N4950 S14.3.1.2], exists to prevent a derived class from accessing Protected members of sibling instances. If Base& b happened to refer to a Derived2 object that Also inherits from BaseAllowing access to b.value_ would violate encapsulation.
This topic covers the essential concepts and techniques related to access control and friendship, including key principles and practical applications.
Key concepts include:
core concepts and definitions key principles and frameworks practical applications common techniques and methods evaluation and critical analysis A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.