C++ Class Construct  «Prev  Next»
Lesson 7 Nested classes
Objective Examine some examples of nested classes in C++23

Building Nested Classes in C++

Nested classes represent a powerful organizational feature in C++ that allows developers to define one class within the scope of another. This capability, which has evolved significantly from C's nested structs, provides enhanced encapsulation, improved code organization, and clearer expression of relationships between types. In modern C++23, nested classes integrate seamlessly with concepts, modules, and other contemporary language features to create more maintainable and expressive code architectures.

Understanding Nested Class Fundamentals

At its core, a nested class is simply a class declared within another class's definition. Unlike C's nested structs where inner structure names were visible externally, C++ nested classes respect proper encapsulation boundaries and scope resolution. The outer class acts as a namespace for the inner class, creating a clear hierarchical relationship that reflects the logical organization of your design.
Consider the following fundamental example that demonstrates scope resolution with nested classes:
char c;      // External scope: ::c

class X {     // Outer class declaration: X::
public:
   class Y {  // Inner class declaration: X::Y::
   public:
      void foo(char e) { 
         ::c = X::c = c = e;  // Three different variables
      }
   private:
      char c;  // X::Y::c (innermost scope)
   };

private:
   char c;     // X::c (outer class scope)
};
In this example, three distinct variables named c exist in different scopes. Within the foo() member function of class Y, the scope resolution operator :: disambiguates which variable is being accessed. The unqualified c refers to X::Y::c, the member of the inner class. The qualified X::c accesses the outer class's private member, demonstrating that nested classes have access to all members of their enclosing class. Finally, ::c references the global variable in the outermost scope. This scope resolution mechanism is fundamental to understanding how nested classes interact with their surrounding context.

Access Control and Encapsulation

One of the most important aspects of nested classes involves understanding their access relationships. A nested class is a member of its enclosing class and follows standard access control rules based on where it's declared. However, the relationship between nested and enclosing classes has several nuances that deserve careful attention.
A nested class has complete access to all members of its enclosing class, including private and protected members. This access exists regardless of whether the nested class is declared public, protected, or private. This behavior makes sense from an encapsulation perspective: the nested class is part of the implementation of the outer class and should have access to its internal state.
class Outer {
private:
   int privateData = 42;
   static inline int sharedCounter = 0;

public:
   class Inner {
   public:
      void accessOuterMembers(Outer& obj) {
         // Can access private members of Outer
         obj.privateData = 100;
         ++Outer::sharedCounter;
      }
   };
};

Conversely, the outer class can only access public members of its nested class. The nested class maintains its own encapsulation boundaries. This asymmetric relationship means that while nested classes can peek into the outer class's private implementation, the outer class must respect the nested class's privacy, treating it like any other type.
class Container {
public:
   class Node {
   public:
      void setData(int val) { data = val; }
      int getData() const { return data; }
   private:
      int data;
      Node* next = nullptr;
   };

   void addNode(int value) {
      Node n;
      n.setData(value);       // OK: public member
      // n.data = value;      // ERROR: data is private
      // n.next = nullptr;    // ERROR: next is private
   }
};

Local Classes Within Functions

C++ extends the concept of nested classes to allow class definitions within function bodies, creating local classes with highly restricted scope. These locally-scoped classes are unavailable outside their defining block, providing a mechanism for creating types that are truly implementation details of a specific function.
void processData() {
   class LocalProcessor {
   public:
      void process(int value) { result = value * 2; }
      int getResult() const { return result; }
   private:
      int result = 0;
   };

   LocalProcessor processor;
   processor.process(21);
   // Use processor within this function...
}

// LocalProcessor y;  // ERROR: LocalProcessor is not visible here
Local classes come with significant restrictions that limit their utility. All member functions must be defined inline within the class definition itself—you cannot separate the declaration from the definition. Local classes cannot access non-static local variables from the enclosing function scope, though they can access static variables, enums, and type definitions from the enclosing scope. These restrictions stem from the fact that local classes don't have external linkage and cannot be named outside their defining scope.
In modern C++, lambda expressions and function objects often provide more flexible alternatives to local classes. However, local classes still have their place when you need a complete type with multiple member functions and private state that won't escape the function boundary.

Nested Function Definitions

C++ does not support true nested functions in the way that languages like Pascal or Python do. However, nested classes provide a restricted form of function nesting capability. By defining classes within classes, you can create member functions that are only accessible within a specific scope hierarchy, effectively simulating nested function behavior within an object-oriented framework.
class AlgorithmSuite {
public:
   void performComplexOperation() {
      class Helper {
      public:
         static int calculateSubResult(int a, int b) {
            return a * a + b * b;
         }
      };

      int result = Helper::calculateSubResult(3, 4);
      // Helper is only visible within this function
   }
};

Modern C++ developers typically prefer lambda expressions for simple nested function-like behavior, as lambdas offer more concise syntax and can capture variables from enclosing scope. However, nested classes within functions remain useful when you need multiple related helper functions with shared state or when the functionality is complex enough to warrant a full class definition.

Practical Use Cases and Design Patterns

One of the most common applications of nested classes is the Iterator pattern. Container classes frequently define nested iterator types that provide controlled access to the container's elements. This design encapsulates the iteration mechanism while keeping it logically associated with the container itself.
template<typename T>
class LinkedList {
private:
   struct Node {
      T data;
      Node* next;
      Node(T val) : data(val), next(nullptr) {}
   };

   Node* head = nullptr;

public:
   class Iterator {
   public:
      using iterator_category = std::forward_iterator_tag;
      using value_type = T;
      using difference_type = std::ptrdiff_t;
      using pointer = T*;
      using reference = T&;

      explicit Iterator(Node* node) : current(node) {}

      T& operator*() { return current->data; }
      
      Iterator& operator++() {
         current = current->next;
         return *this;
      }

      bool operator==(const Iterator& other) const {
         return current == other.current;
      }

   private:
      Node* current;
   };

   Iterator begin() { return Iterator(head); }
   Iterator end() { return Iterator(nullptr); }
};
The Pimpl (Pointer to Implementation) idiom often uses nested classes to hide implementation details. By forward-declaring a nested class and defining it only in the implementation file, you can completely hide private implementation details from header files, reducing compilation dependencies and improving encapsulation.
// Widget.h
class Widget {
public:
   Widget();
   ~Widget();
   void doSomething();

private:
   class Impl;  // Forward declaration
   std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
class Widget::Impl {
public:
   void doSomethingImpl() {
      // Complex implementation hidden from header
   }
private:
   // Private data members not visible in header
   std::vector<int> data;
   std::string internalState;
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::doSomething() { pImpl->doSomethingImpl(); }
Nested classes excel at implementing the State pattern, where an object's behavior changes based on its internal state. By defining state classes as nested types, you keep related states grouped together within the context class, improving code organization and maintainability.
class NetworkConnection {
private:
   class State {
   public:
      virtual ~State() = default;
      virtual void connect(NetworkConnection& conn) = 0;
      virtual void disconnect(NetworkConnection& conn) = 0;
      virtual std::string getStatus() const = 0;
   };

   class DisconnectedState : public State {
   public:
      void connect(NetworkConnection& conn) override;
      void disconnect(NetworkConnection&) override {}
      std::string getStatus() const override { 
         return "Disconnected"; 
      }
   };

   class ConnectedState : public State {
   public:
      void connect(NetworkConnection&) override {}
      void disconnect(NetworkConnection& conn) override;
      std::string getStatus() const override { 
         return "Connected"; 
      }
   };

   std::unique_ptr<State> currentState;

public:
   NetworkConnection() : 
      currentState(std::make_unique<DisconnectedState>()) {}

   void changeState(std::unique_ptr<State> newState) {
      currentState = std::move(newState);
   }

   void connect() { currentState->connect(*this); }
   void disconnect() { currentState->disconnect(*this); }
};

Modern C++20 and C++23 Features

C++20 concepts integrate seamlessly with nested classes, allowing you to constrain template nested types and enforce compile-time requirements on their structure. This combination enhances type safety while maintaining the organizational benefits of nested classes.
template<typename T>
concept Incrementable = requires(T t) {
   { ++t } -> std::same_as<T&>;
};

template<typename T>
class Container {
public:
   class Iterator requires Incrementable<T> {
   public:
      Iterator& operator++() {
         ++value;
         return *this;
      }
      T& operator*() { return value; }
   private:
      T value;
   };
};
C++23 enhances constexpr capabilities, allowing more complex compile-time computations with nested classes. You can now define nested classes with constexpr constructors and member functions that participate fully in constant expression evaluation.
class CompileTimeCalculator {
public:
   class Operation {
   public:
      constexpr Operation(int a, int b) : x(a), y(b) {}
      
      constexpr int add() const { return x + y; }
      constexpr int multiply() const { return x * y; }
      
   private:
      int x, y;
   };

   static constexpr int compute() {
      constexpr Operation op(10, 20);
      return op.add() + op.multiply();  // 230
   }
};

static_assert(CompileTimeCalculator::compute() == 230);
C++20 modules change how nested classes interact with visibility and compilation boundaries. When defining nested classes in module interface files, you control their visibility through export declarations, providing fine-grained control over your API surface.
// mymodule.ixx
export module mymodule;

export class Outer {
public:
   class PublicNested {  // Automatically exported
   public:
      void doWork();
   };

private:
   class PrivateNested {  // Not visible outside module
      void internalWork();
   };
};

Template Nested Classes

Nested classes can be templates themselves, and template classes can contain nested classes. This combination provides powerful abstraction capabilities for generic programming while maintaining clear type relationships.

template<typename T>
class SmartContainer {
private:
   template<typename U>
   class Node {
   public:
      U data;
      Node<U>* next;
      
      Node(U val) : data(val), next(nullptr) {}
   };

   Node<T>* head;

public:
   template<typename U>
   class TypedIterator {
   public:
      explicit TypedIterator(Node<U>* node) : current(node) {}
      
      U& operator*() { return current->data; }
      TypedIterator& operator++() {
         current = current->next;
         return *this;
      }
      
   private:
      Node<U>* current;
   };

   using iterator = TypedIterator<T>;
};

Friend Relationships and Nested Classes

Friend declarations interact with nested classes in specific ways that affect access control. A nested class can be declared as a friend of another class, granting it privileged access. However, friendship is not inherited—making an outer class a friend doesn't automatically grant friendship to its nested classes.
class Protected {
private:
   int secret = 42;

   friend class Accessor::Helper;  // Specific nested class
};

class Accessor {
public:
   class Helper {
   public:
      int getSecret(Protected& p) {
         return p.secret;  // OK: Helper is friend
      }
   };

   int tryGetSecret(Protected& p) {
      // return p.secret;  // ERROR: Accessor itself is not friend
      Helper h;
      return h.getSecret(p);  // Must use Helper
   }
};

Performance Considerations

Nested classes themselves introduce no runtime performance overhead compared to non-nested classes. The nesting is purely a scoping and organizational construct resolved at compile time. However, design decisions around nested classes can affect performance in subtle ways.
Defining nested class member functions inline within the class definition can improve optimization opportunities, especially for small, frequently-called functions. The compiler has better visibility into the implementation and can make more aggressive inlining decisions.
When using nested classes for implementation patterns like iterators or state objects, consider memory layout and cache locality. Nested classes that access outer class members frequently benefit from careful attention to data layout to minimize cache misses.


Best Practices and Guidelines

Use nested classes when a class is tightly coupled to its enclosing class and has no standalone meaning outside that context. Iterator classes, implementation classes for the Pimpl idiom, and state classes in the State pattern are excellent candidates. Avoid nested classes when the type might be useful independently or when the nesting significantly increases complexity without corresponding organizational benefits.
Follow clear naming conventions for nested classes. Many codebases prefix nested class names to indicate their relationship or role, such as Node for container implementation details or Iterator for iteration support. Keep nested class names concise since they're already namespaced by the outer class.
When using the Pimpl idiom with nested classes, forward-declare the nested class in the header and define it only in the implementation file. This maximizes encapsulation and minimizes compilation dependencies. Remember that you need to explicitly default or define the destructor in the implementation file when using std::unique_ptr with forward-declared types.
// Header file
class Widget {
public:
   Widget();
   ~Widget();  // Must be declared, defined in .cpp

private:
   class Impl;
   std::unique_ptr<Impl> pImpl;
};
Document nested classes thoroughly, explaining their relationship to the enclosing class and any constraints on their use. Since nested classes can access private members of their enclosing class, document which members they're expected to interact with and any invariants they must maintain.

Common Pitfalls and Solutions

One common mistake involves trying to use nested classes before they're fully defined. Remember that a nested class isn't fully defined until the outer class definition is complete, which can cause issues with circular dependencies.
class Outer {
public:
   class Inner;  // Forward declaration

   void useInner(Inner* ptr);  // OK: pointer only
   // void useInner(Inner obj);  // ERROR: Inner incomplete

   class Inner {
   public:
      void method();
   };
};

// Define member function after Inner is complete
void Outer::useInner(Inner* ptr) {
   ptr->method();  // OK now
}
Developers sometimes expect symmetric access between nested and outer classes. Remember: nested classes can access all outer class members, but outer classes can only access public members of nested classes. Don't violate encapsulation by making nested class members public solely for outer class access—use friend declarations if necessary.
With template nested classes, pay attention to instantiation requirements. A nested class template isn't instantiated until it's actually used, which can delay error detection.

Alternatives to Nested Classes

For purely organizational purposes, namespaces often provide a cleaner alternative to nested classes. If the inner class doesn't need access to private members of the outer class, consider using namespaces instead.
// Instead of:
class Graphics {
public:
   class Color { /*...*/ };
   class Point { /*...*/ };
};

// Consider:
namespace Graphics {
   class Color { /*...*/ };
   class Point { /*...*/ };
}
For state pattern implementations, C++17's std::variant combined with std::visit provides a modern alternative that leverages type safety and avoids virtual function overhead.
class Connection {
   struct Disconnected {};
   struct Connecting { int progress; };
   struct Connected { std::string sessionId; };

   std::variant<Disconnected, Connecting, Connected> state;

public:
   std::string getStatus() {
      return std::visit([](auto&& s) -> std::string {
         using T = std::decay_t<decltype(s)>;
         if constexpr (std::is_same_v<T, Disconnected>)
            return "Disconnected";
         else if constexpr (std::is_same_v<T, Connecting>)
            return "Connecting...";
         else
            return "Connected";
      }, state);
   }
};

Conclusion

Nested classes represent a sophisticated organizational tool in C++ that bridges object-oriented encapsulation with modern language features. From basic scope resolution to advanced template metaprogramming, nested classes provide mechanisms for expressing tight coupling between types while maintaining clean API boundaries. Understanding when to use nested classes versus alternatives like namespaces, lambdas, or std::variant requires careful consideration of encapsulation needs, access patterns, and organizational clarity. As C++ continues evolving with features like concepts, modules, and enhanced compile-time computation, nested classes remain relevant and increasingly powerful tools for expressing complex relationships between types in maintainable, performant code.

SEMrush Software