What Are Nested Types (Member Types)?

A class body can hold three kinds of member, not two. Data members and member functions are the familiar pair. The third is a nested type, also called a member type: a type definition written inside the class, under an access specifier, exactly like any other member.

Anything that names a type qualifies. An enumeration, a type alias, or a whole second class can all be declared inside a class body, and once declared they belong to that class the way a data member does. They obey its access specifiers, they are reached through its name from outside, and they disappear from the surrounding namespace entirely.

Kind of member Written as Reached from outside as
Data member int m_holdMinutes{}; firing.holdMinutes(), through the interface
Member function int peakCelsius() const; firing.peakCelsius()
Member type enum Stage { ... }; or using Celsius = int; Kiln::Stage, Kiln::Celsius

Why Put a Type Inside a Class

Say a kiln controller has an enumeration of firing stages. Left at namespace scope, that enumeration is a free-floating type that happens to be passed to a kiln constructor. Nothing in the code states that the two belong together, so the connection lives only in the head of whoever wrote it, and the name has to carry the whole burden of the relationship, which is why such types end up called things like KilnStage.

Moving the enumeration inside the class states the relationship in the one place a reader always looks: the class definition. Three things follow from that move.

  • The type is documented as part of the class rather than as a neighbour of it.
  • The class name becomes the scope, so the type can drop the prefix it was carrying and be called Stage, read as Kiln::Stage from outside.
  • Access control now applies. A member type under private: is an implementation detail nobody outside can even name.

An Enumeration That Belongs to One Class

Here is the kiln with its stage enumeration moved inside:

#include <iostream>

class Kiln
{
public:
    enum Stage
    {
        bisque,
        glaze,
        lustre,
    };

private:
    Stage m_stage{};
    int m_peakCelsius{};

public:
    Kiln(Stage stage, int peakCelsius)
        : m_stage{ stage }, m_peakCelsius{ peakCelsius }
    {
    }

    int peakCelsius() const { return m_peakCelsius; }

    // Inside the class the enumerator needs no qualification
    bool needsSlowCool() const { return m_stage == glaze; }
};

int main()
{
    const Kiln firstFiring{ Kiln::bisque, 980 };
    const Kiln secondFiring{ Kiln::glaze, 1240 };

    std::cout << std::boolalpha;
    std::cout << firstFiring.peakCelsius() << " C, slow cool: " << firstFiring.needsSlowCool() << '\n';
    std::cout << secondFiring.peakCelsius() << " C, slow cool: " << secondFiring.needsSlowCool() << '\n';

    return 0;
}

Output:

980 C, slow cool: false
1240 C, slow cool: true

Stage sits under public:, so the outside world may use it. Had it been written under private:, the class could still use it internally, but Kiln::bisque in main() would be rejected the same way a private data member would be.

The Class Is the Scope Region

A class does the same job for the names inside it that a namespace does: it is a scope region. That single fact determines how every nested name is spelled.

Where the name is used What you write
Inside a member of Kiln Stage, bisque
Anywhere outside Kiln Kiln::Stage, Kiln::bisque
Outside, with a nested scoped enum Kiln::Stage::bisque

Member functions are already inside the scope region, which is why needsSlowCool() compares against a bare glaze. Outside the class there is no such shortcut, and the unqualified name is simply not visible. This example is broken on purpose:

#include <iostream>

class Kiln
{
public:
    enum Stage
    {
        bisque,
        glaze,
        lustre,
    };
};

int main()
{
    const Kiln::Stage next{ glaze };

    std::cout << next << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:16:29: error: 'glaze' was not declared in this scope
   16 |     const Kiln::Stage next{ glaze };
      |                             ^~~~~

The third row of that table explains a habit that looks backwards at first. Everywhere else, a scoped enumeration (enum class) is the default choice, because it keeps its enumerators out of the surrounding scope. Nested in a class, the enclosing class is already doing that job, so a scoped enum adds a second layer of qualification and turns Kiln::bisque into Kiln::Stage::bisque. Nested enumerations are therefore usually left unscoped, and you would reach for enum class here only when you also want the stricter conversion rules it brings.

The naming point is worth stating too. Because the class name is always part of the qualified name, repeating it in the type name reads badly: Kiln::KilnStage says kiln twice. Rename the type to what it is once the class has supplied the context, and Kiln::Stage reads like a sentence.

Declare Before You Use

A nested type is a name like any other, and names have to be declared before the compiler meets them. Since member declarations are read in order, a member type placed below the members that use it is too late. This example is broken on purpose:

#include <iostream>

class Kiln
{
private:
    Stage m_stage{};

public:
    enum Stage
    {
        bisque,
        glaze,
        lustre,
    };
};

int main()
{
    std::cout << sizeof(Kiln) << '\n';

    return 0;
}
s.cpp:6:5: error: 'Stage' does not name a type
    6 |     Stage m_stage{};
      |     ^~~~~

Member function bodies are the exception. They are compiled as though they appeared after the class was complete, so a body may freely use a type declared further down. Every other position, including data member declarations, parameter types, and return types, is read in order and needs the name already in hand.

Best Practice
Put nested types at the very top of the class definition. They are the vocabulary the rest of the class is written in, and placing them first means no member can ever refer to one before it exists.

Type Aliases as Members

The same mechanism accepts a type alias. Here the kiln names the unit its temperatures are measured in, so the alias travels with the class:

#include <iostream>

class Kiln
{
public:
    enum Stage
    {
        bisque,
        glaze,
        lustre,
    };

    using Celsius = int;

private:
    Stage m_stage{};
    Celsius m_peakCelsius{};

public:
    Kiln(Stage stage, Celsius peakCelsius)
        : m_stage{ stage }, m_peakCelsius{ peakCelsius }
    {
    }

    Celsius peakCelsius() const { return m_peakCelsius; } // unqualified inside the class
};

int main()
{
    const Kiln secondFiring{ Kiln::glaze, 1240 };
    const Kiln::Celsius peak{ secondFiring.peakCelsius() }; // qualified outside it

    std::cout << "Peak reached " << peak << " C" << '\n';

    return 0;
}

Output:

Peak reached 1240 C

The qualification rule is unchanged: Celsius inside the class, Kiln::Celsius outside it. What the alias buys is the same thing any type alias buys, with one addition. Callers can write Kiln::Celsius instead of hard-coding int, so if the class later switches to double, code written against the alias follows along.

The standard library leans on this heavily. std::string alone declares more than a dozen nested aliases, among them size_type, value_type, iterator, and const_iterator, and generic code depends on every container spelling those names the same way.

A Class Inside a Class

A class may also be nested inside another. This is the least common of the three, and the one with a rule people trip over: a nested class has no access to the outer class's this pointer. It is a member of the outer class, not a part of any particular outer object, and it can be created entirely on its own with no outer object anywhere in sight.

So reaching for an outer member by name from inside a nested class fails. This example is broken on purpose:

#include <iostream>

class Kiln
{
public:
    class Certificate
    {
    public:
        void print() const
        {
            std::cout << "Fired to " << m_peakCelsius << " C" << '\n';
        }
    };

private:
    int m_peakCelsius{};

public:
    explicit Kiln(int peakCelsius) : m_peakCelsius{ peakCelsius } {}
};

int main()
{
    const Kiln::Certificate certificate{};

    certificate.print();

    return 0;
}
s.cpp: In member function 'void Kiln::Certificate::print() const':
s.cpp:11:41: error: invalid use of non-static data member 'Kiln::m_peakCelsius'
   11 |             std::cout << "Fired to " << m_peakCelsius << " C" << '\n';
      |                                         ^~~~~~~~~~~~~
s.cpp:16:9: note: declared here
   16 |     int m_peakCelsius{};
      |         ^~~~~~~~~~~~~

The fix is to pass an outer object in. Once there is a Kiln to talk about, the nested class's membership pays off: being a member of Kiln, it may read Kiln's private members without any friend declaration.

#include <iostream>

class Kiln
{
public:
    using Celsius = int;

    class Certificate
    {
    public:
        void print(const Kiln& kiln) const
        {
            // Certificate is a member of Kiln, so Kiln's private members are in reach,
            // but only through a Kiln object handed to us
            std::cout << "Fired to " << kiln.m_peakCelsius << " C over " << kiln.m_holdMinutes << " minutes" << '\n';
        }
    };

private:
    Celsius m_peakCelsius{};
    int m_holdMinutes{};

public:
    Kiln(Celsius peakCelsius, int holdMinutes)
        : m_peakCelsius{ peakCelsius }, m_holdMinutes{ holdMinutes }
    {
    }
};

int main()
{
    const Kiln secondFiring{ 1240, 25 };
    const Kiln::Certificate certificate{};

    certificate.print(secondFiring);

    return 0;
}

Output:

Fired to 1240 C over 25 minutes
Key Concept
Membership and ownership are different things. A nested class is a member of the outer class, which is what grants it access to private members. It is not part of an outer object, which is why it never has one to reach for automatically.

The standard library's one heavy use of nested classes is iterators: the type that walks a container is almost always declared inside that container, which is why you see names like std::string::iterator. A later chapter covers what iterators do.

Forward Declaring a Nested Type

Nested types follow the usual declare-now, define-later pattern, with one boundary. Inside the enclosing class you may declare a nested type and define it later, either still inside the class or out at namespace scope with a qualified name:

#include <iostream>

class Kiln
{
public:
    class Sensor;   // declared here
    class Sensor{}; // and defined here, still inside Kiln

    class Certificate; // declared here, defined below
};

class Kiln::Certificate // a nested type may be defined outside its enclosing class
{
};

int main()
{
    std::cout << sizeof(Kiln::Sensor) << ' ' << sizeof(Kiln::Certificate) << '\n';

    return 0;
}

Output:

1 1

What you cannot do is mention a nested type before the enclosing class has been defined. The qualified name Kiln::Sensor requires the compiler to look inside Kiln, and there is nothing to look inside of yet. This example is broken on purpose:

#include <iostream>

class Kiln;         // fine: an ordinary forward declaration
class Kiln::Sensor; // not fine: Kiln is still incomplete

class Kiln
{
public:
    class Sensor{};
};

int main()
{
    std::cout << sizeof(Kiln::Sensor) << '\n';

    return 0;
}
s.cpp:4:13: error: invalid use of incomplete type 'class Kiln'
    4 | class Kiln::Sensor; // not fine: Kiln is still incomplete
      |             ^~~~~~
s.cpp:3:7: note: forward declaration of 'class Kiln'
    3 | class Kiln;         // fine: an ordinary forward declaration
      |       ^~~~

Moving that line below the class definition makes it legal, but pointless: the class definition already declared Sensor, so a second declaration adds nothing.

Summary

Point What to remember
What a nested type is A type defined inside a class body under an access specifier, making it a member of that class
What can be nested Enumerations, type aliases (using), and other classes
Why nest one It records that the type exists for this class, lets the class name carry the context, and puts the type under access control
Naming inside the class Unqualified: Stage, Celsius, bisque
Naming outside the class Qualified with the class: Kiln::Stage, Kiln::Celsius, Kiln::bisque
Scoped or unscoped enum Prefer unscoped when nesting, since the class already supplies the scope and a scoped enum would force Kiln::Stage::bisque
Ordering Declarations are read in order, so define nested types at the top of the class. Member function bodies are the one place a later name may be used
Nested classes and this A nested class has no outer this. Pass an outer object in when it needs one
Nested classes and privacy As a member of the outer class, a nested class may read the outer class's private members of any object it is given
Standard library use Nested aliases are everywhere (std::string declares more than a dozen), and iterators are the classic nested class
Forward declaration Legal inside the enclosing class, with the definition following inside or outside. Illegal before the enclosing class is defined

Nested types are the quiet organisational tool of class design. They cost nothing at runtime, they keep a class's vocabulary attached to the class instead of scattered around it, and once you are writing your own containers and templates they stop being optional, because the rest of the language expects to find value_type and iterator exactly where the class put them.