Access Control and Friendship
Access Control and Friendship
Section titled “Access Control and Friendship”C++ provides fine-grained access control through public``protectedAnd private specifiers, Plus the friend mechanism for granting selective access to non-members. Access control is enforced Entirely at compile time with zero runtime cost.
2.1 Access Specifiers [N4950 S14.2]
Section titled “2.1 Access Specifiers [N4950 S14.2]”A class member can be declared with one of three access specifiers [N4950 S14.3.1]:
| Specifier | Class members | Derived class members | External code |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
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.
Formal definition. A member of a class is accessible from a particular point in the program if And only if the access rules in [N4950 S14.3] permit it. The three access specifiers define the Following accessibility sets:
public: the member is a member of the access interface of the class and is accessible from anywhere the class itself is accessible [N4950 S14.3.1.1].protected: the member is accessible from member functions and friends of the class, and from member functions and friends of derived classes [N4950 S14.3.1.2].private: the member is accessible only from member functions and friends of the class that declares it [N4950 S14.3.1.3].
#include <cstdio>
class Account {private: double balance_ = 0.0;
void log(const char* msg) const { std::printf("[LOG] %s: balance=%.2f\n", msg, balance_); }
protected: void set_balance(double b) { balance_ = b; }
public: void deposit(double amount) { balance_ += amount; log("deposit"); }
double balance() const { return balance_; }};
class SavingsAccount : public Account {public: void apply_interest(double rate) { set_balance(balance() * (1.0 + rate)); // protected: accessible }};
int main() { Account a; a.deposit(100.0); // a.set_balance(50.0); // error: protected // a.balance_ = 0; // error: private // a.log("test"); // error: private
SavingsAccount s; s.deposit(200.0); s.apply_interest(0.05);}Proof: Access Control is Compile-Time Only
Section titled “Proof: Access Control is Compile-Time Only”The access rules in [N4950 S14.3] apply during name lookup and access checking, which are phases Of translation (compilation). The generated object code contains no guards, checks, or indirections Related to access control. Therefore, the cost is provably zero at runtime: the access specifier Does not affect the object layout, function calling convention, or any aspect of the execution Model.
2.2 Class vs Struct
Section titled “2.2 Class vs Struct”In C++, class and struct are identical except for one default: in a classMembers are private by default; in a structMembers are public by default [N4950 S13.3].
struct S { int x; }; // x is publicclass C { int x; }; // x is private
static_assert(sizeof(S) == sizeof(C));:::tip Convention Use struct for passive data aggregates with no invariant enforcement. Use class for types that maintain invariants and require encapsulation.
2.3 Friend Declarations [N4950 S14.3]
Section titled “2.3 Friend Declarations [N4950 S14.3]”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.
Formal Semantics of Friendship [N4950 S14.3.2]
Section titled “Formal Semantics of Friendship [N4950 S14.3.2]”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
frienddeclaration is the granting class. The nominated entity receives access; no reciprocal access is implied. - Not transitive. If
AdeclaresBas a friend, andBdeclaresCas a friend,Cdoes not have access toA’s private members. - Not inherited. If
AdeclaresBas a friend, andCinherits fromB``Cdoes not have access toA’s private members. - Friendship is not a membership declaration. A friend function is not a member of the class. It does not have a
thispointer and is not found by name lookup within the class scope (unless it is also declared as a member).
#include <cstdio>
class Matrix;
class Vector { double data_[3]{}; friend Vector operator*(const Matrix& m, const Vector& v);public: 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]; }};
class Matrix { double m_[3][3]{}; friend Vector operator*(const Matrix& m, const Vector& v);public: Matrix(std::initializer_list<std::initializer_list<double>> il) { std::size_t i = 0; for (auto row : il) { std::size_t j = 0; for (auto val : row) { m_[i][j++] = val; } ++i; } }};
Vector operator*(const Matrix& m, const Vector& v) { Vector result{0, 0, 0}; for (std::size_t i = 0; i < 3; ++i) { double sum = 0; for (std::size_t j = 0; j < 3; ++j) { sum += m.m_[i][j] * v.data_[j]; } result.data_[i] = sum; } return result;}
int main() { Matrix m{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; Vector v{1, 2, 3}; Vector r = m * v; std::printf("result: %.1f %.1f %.1f\n", r[0], r[1], r[2]);}Proof: Friend Access Rules
Section titled “Proof: Friend Access Rules”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.
2.4 Friend Classes and Member Functions
Section titled “2.4 Friend Classes and Member Functions”Friendship can be granted to an entire class or to a specific member function of another class:
#include <cstdio>
class Engine;
class DiagnosticTool {public: void inspect(const Engine& e);};
class Engine { int rpm_ = 0; double temp_ = 0.0; friend class Mechanic; // entire class is a friend friend void DiagnosticTool::inspect(const Engine&); // one memberpublic: void set_rpm(int rpm) { rpm_ = rpm; } void set_temp(double t) { temp_ = t; }};
class Mechanic {public: void tune(Engine& e) { e.rpm_ = 800; // OK: Mechanic is a friend e.temp_ = 90.0; 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_);}
int main() { Engine e; Mechanic m; m.tune(e);
DiagnosticTool d; d.inspect(e);}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.
2.5 Hidden Friends (ADL Friends) [N4950 S14.3.3]
Section titled “2.5 Hidden Friends (ADL Friends) [N4950 S14.3.3]”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].
#include <cstdio>
class Metric { double value_; 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) { return m.value_; }public: explicit Metric(double v) : value_(v) {}};
int main() { Metric a{3.0}, b{4.0}; 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.
2.6 When to Use Friendship
Section titled “2.6 When to Use Friendship”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&)orVector 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.
2.7 Access Control and Inheritance
Section titled “2.7 Access Control and Inheritance”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.
Public, Protected, and Private Inheritance
Section titled “Public, Protected, and Private Inheritance”#include <iostream>
class Base {public: int pub = 1;protected: int prot = 2;private: int priv = 3;};
class PubDerived : public Base {public: void test() { 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 {public: void test() { std::cout << pub << "\n"; // OK: still accessible within PrivDerived std::cout << prot << "\n"; // OK: still accessible within PrivDerived }};
int main() { PubDerived pd; std::cout << pd.pub << "\n"; // OK: public inheritance preserves public access // std::cout << pd.prot << "\n"; // ERROR: protected, not accessible outside
PrivDerived prd; // std::cout << prd.pub << "\n"; // ERROR: private inheritance makes everything private}Inheritance Access Summary
Section titled “Inheritance Access Summary”| Base Member Access | Public Inheritance | Protected Inheritance | Private Inheritance |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | inaccessible | 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.
The using Declaration in Derived Classes
Section titled “The using Declaration in Derived Classes”You can restore the access level of inherited members with a using declaration [N4950 S11.4.6]:
class PrivateBase {public: void public_func() {}protected: void protected_func() {}};
class Adapter : private PrivateBase {public: using PrivateBase::public_func; // Restore public access using PrivateBase::protected_func; // Also restore protected to public};
int main() { Adapter a; 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.
2.8 Access Control and Templates
Section titled “2.8 Access Control and Templates”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.
#include <iostream>
class Secret { int value_ = 42; template<typename T> friend void inspect(T&);};
template<typename T>void inspect(T& obj) { // 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";}
int main() { Secret s; inspect(s);}CRTP and Private Members
Section titled “CRTP and Private Members”The Curiously Recurring Template Pattern (CRTP) commonly requires the derived class to access Private members of the base:
#include <iostream>
template<typename Derived>class Counter { int count_ = 0;protected: void increment() { ++count_; } int count() const { return count_; }};
class Widget : private Counter<Widget> { friend class Counter<Widget>;public: void click() { increment(); } int clicks() const { return count(); }};
int main() { Widget w; w.click(); w.click(); std::cout << "Clicks: " << w.clicks() << "\n"; // Output: 2}2.9 final Specifier [N4950 S11.7.4]
Section titled “2.9 final Specifier [N4950 S11.7.4]”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 markedfinalshall not be used as a base class. - Member function
final: A virtual function markedfinalshall not be overridden in any derived class.
#include <iostream>
class Base {public: virtual void process() { std::cout << "Base::process\n"; } virtual ~Base() = default;};
class Final : public Base {public: void process() final { std::cout << "Final::process\n"; }};
// class Derived : public Final {}; // ERROR: cannot derive from 'final' classThe final specifier on a virtual function prevents further overriding in derived classes:
class Mid : public Base {public: void process() override final { std::cout << "Mid::process\n"; }};
// class Leaf : public Mid {// void process() override {} // ERROR: process is final// };final Enables Devirtualization
Section titled “final Enables Devirtualization”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.
2.10 Nested Access and Friends of Nested Classes [N4950 S14.3]
Section titled “2.10 Nested Access and Friends of Nested Classes [N4950 S14.3]”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
privateandprotectedmembers). 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.
#include <iostream>
class Outer { int secret_ = 99; class Inner { int inner_secret_ = 42; public: 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 enclosingpublic: void test() { Inner i; // i.inner_secret_; // ERROR: enclosing class cannot access nested's private i.access_outer(*this); // OK }};Friend Declarations in Nested Classes
Section titled “Friend Declarations in Nested Classes”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:
#include <iostream>
class Outer { int outer_priv = 1; class Inner { int inner_priv = 2; friend void inner_friend(Inner& i); public: void show(Outer& o) { 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}2.11 Access Control and constexpr/consteval Functions
Section titled “2.11 Access Control and constexpr/consteval Functions”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.
class Vault { int code_ = 1337; friend int break_in(const Vault&);};
consteval int break_in(const Vault& v) { return v.code_; // OK: friend access, evaluated at compile time}
int main() { constexpr int result = break_in(Vault{}); static_assert(result == 1337);}2.12 Access Control and using Directives vs using Declarations
Section titled “2.12 Access Control and using Directives vs using Declarations”A using declaration (inside a class) affects access, but a using directive (at namespace Scope) does not bypass class access control:
#include <iostream>
namespace lib { class Secret { int data_ = 42; friend void reveal(const Secret&); public: void public_method() const { std::cout << "public\n"; } };
void reveal(const Secret& s) { std::cout << s.data_ << "\n"; }}
int main() { using namespace lib;
Secret s; s.public_method(); // OK // reveal(s); // OK: reveal is found by ordinary lookup after using-directive}2.13 Access Specifiers and inline Functions
Section titled “2.13 Access Specifiers and inline Functions”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.
Common Pitfalls
Section titled “Common Pitfalls”- Assuming friendship is transitive or inherited. If
AdeclaresBas a friend, andCinherits fromB``Cdoes not have access toA’s private members. Each class controls its own friendship independently. - Using
protecteddata members. While syntactically legal,protecteddata members break encapsulation because any derived class can modify them directly without the base class’s knowledge. Preferprotectedmember functions (getters/setters) orprivatedata withprotectedaccessors. - Forgetting that
classdefaults toprivateandstructdefaults topublic. Astructwith no access specifier has public members by default, which can accidentally expose implementation details. Always be explicit about access specifiers. - Overusing friendship. Every
frienddeclaration 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
protectedmember 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
usingdeclaration 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 ausingdeclaration cannot target a single overload.
2.14 Access Control and In-Class Member Functions
Section titled “2.14 Access Control and In-Class Member Functions”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:
#include <iostream>
class SecretHolder { int secret_ = 42;
void helper() { std::cout << "helper: " << secret_ << "\n"; }
public: void reveal() { helper(); // OK: member function accesses private member function std::cout << "secret: " << secret_ << "\n"; }};
int main() { SecretHolder sh; sh.reveal(); // sh.helper(); // ERROR: helper is private // sh.secret_; // ERROR: secret_ is private}2.15 Access Control and Friends of Friends
Section titled “2.15 Access Control and Friends of Friends”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:
#include <iostream>
class A { int data_ = 10; friend void inspect(A& a);};
void inspect(A& a) { std::cout << a.data_ << "\n"; // OK: friend of A}
// A separate function that is NOT a friend of Avoid external(A& a, void (*inspector)(A&)) { inspector(a); // OK: calls the friend function // a.data_; // ERROR: external is not a friend of A}
int main() { A a; inspect(a); external(a, inspect);}2.16 Access Control and Lambda Captures
Section titled “2.16 Access Control and Lambda Captures”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:
#include <iostream>#include <vector>#include <algorithm>
class DataStore { std::vector<int> data_;
public: void add(int val) { data_.push_back(val); }
void process() const { // 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(); }); if (it != data_.end()) { std::cout << "Found: " << *it << " (threshold=" << threshold() << ")\n"; } }
private: int threshold() const { return 5; }};
int main() { DataStore ds; ds.add(3); ds.add(8); ds.add(1); ds.process();}2.17 Protected Access Through Pointers and References [N4950 S14.3.1.2]
Section titled “2.17 Protected Access Through Pointers and References [N4950 S14.3.1.2]”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:
#include <iostream>
class Base {protected: int value_ = 42;};
class Derived : Base {public: void access_own() { 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.
See Also
Section titled “See Also”Summary
Section titled “Summary”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
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
:::