Nested Types (Member Types)

Consider the following program:

#include <iostream>

enum class Shape
{
    circle,
    square,
    triangle
};

class Drawable
{
private:
    Shape m_shape{};
    int m_size{ 10 };

public:
    Drawable(Shape shape)
        : m_shape{ shape }
    {
    }

    Shape getShape() { return m_shape; }
    int getSize() { return m_size; }

    bool isCircle() { return m_shape == Shape::circle; }
};

int main()
{
    Drawable obj{ Shape::circle };

    if (obj.getShape() == Shape::circle)
        std::cout << "I am a circle";
    else
        std::cout << "I am not a circle";

    return 0;
}

There's nothing wrong with this program. However, because enum class Shape is meant to be used in conjunction with the Drawable class, having it exist independently doesn't clearly show their relationship.

Nested types (member types)

So far, we've seen class types with two kinds of members: data members and member functions. Our Drawable class above has both of these.

Class types support another kind of member: nested types (also called member types). To create a nested type, you simply define the type inside the class under the appropriate access specifier.

Here's the same program rewritten to use a nested type defined inside the Drawable class:

#include <iostream>

class Drawable
{
public:
    // Shape has been moved inside the class under the public access specifier
    // We've also renamed it ShapeType and made it an enum rather than an enum class
    enum ShapeType
    {
        circle,
        square,
        triangle
    };

private:
    ShapeType m_shape{};
    int m_size{ 10 };

public:
    Drawable(ShapeType shape)
        : m_shape{ shape }
    {
    }

    ShapeType getShape() { return m_shape; }
    int getSize() { return m_size; }

    bool isCircle() { return m_shape == circle; } // Inside members of Drawable, we no longer need to prefix enumerators with Shape::
};

int main()
{
    // Note: Outside the class, we access the enumerators via the Drawable:: prefix now
    Drawable obj{ Drawable::circle };

    if (obj.getShape() == Drawable::circle)
        std::cout << "I am a circle";
    else
        std::cout << "I am not a circle";

    return 0;
}

There are several things worth noting here.

First, Shape is now defined inside the class, where it has been renamed ShapeType for reasons we'll discuss shortly.

Second, nested type ShapeType is defined at the top of the class. Nested type names must be fully defined before they can be used, so they are usually defined first.

Best Practice
Define any nested types at the top of your class type.

Third, nested types follow normal access rules. ShapeType is defined under the public access specifier, so the type name and enumerators can be directly accessed by the public.

Fourth, class types act as a scope region for names declared within, just as namespaces do. Therefore the fully qualified name of ShapeType is Drawable::ShapeType, and the fully qualified name of the circle enumerator is Drawable::circle.

Within the class members, we don't need to use the fully qualified name. For example, in member function isCircle() we access the circle enumerator without the Drawable:: scope qualifier.

Outside the class, we must use the fully qualified name (e.g., Drawable::circle). We renamed Shape to ShapeType so we can access it as Drawable::ShapeType (rather than the more redundant Drawable::Shape).

Finally, we changed our enumerated type from scoped to unscoped. Since the class itself now acts as a scope region, it's somewhat redundant to use a scoped enumerator as well. Changing to an unscoped enum means we can access enumerators as Drawable::circle rather than the longer Drawable::ShapeType::circle we'd need if the enumerator were scoped.

Nested typedefs and type aliases

Class types can also contain nested typedefs or type aliases:

#include <iostream>
#include <string>
#include <string_view>

class Product
{
public:
    using PriceType = double;

private:
    std::string m_name{};
    PriceType m_price{};
    int m_quantity{};

public:
    Product(std::string_view name, PriceType price, int quantity)
        : m_name{ name }
        , m_price{ price }
        , m_quantity{ quantity }
    {
    }

    const std::string& getName() { return m_name; }
    PriceType getPrice() { return m_price; } // can use unqualified name within class
    int getQuantity() { return m_quantity; }
};

int main()
{
    Product item{ "Widget", 19.99, 100 };
    Product::PriceType price{ item.getPrice() }; // must use fully qualified name outside class

    std::cout << item.getName() << " costs: $" << price << '\n';

    return 0;
}

This prints:

Widget costs: $19.99

Note that inside the class we can just use PriceType, but outside the class we must use the fully qualified name Product::PriceType.

We discuss the benefits of type aliases in lesson 10.7 -- Typedefs and type aliases, and they serve the same purpose here. It is very common for classes in the C++ standard library to use nested typedefs. As of the time of writing, std::string defines ten nested typedefs!

Nested classes and access to outer class members

It is fairly uncommon for classes to have other classes as a nested type, but it is possible. In C++, a nested class does not have access to the this pointer of the outer (containing) class, so nested classes cannot directly access the members of the outer class. This is because a nested class can be instantiated independently of the outer class (and in such a case, there would be no outer class members to access!).

However, because nested classes are members of the outer class, they can access any private members of the outer class that are in scope.

Let's illustrate with an example:

#include <iostream>
#include <string>
#include <string_view>

class Product
{
public:
    using PriceType = double;

    class Formatter
    {
    public:
        void print(const Product& product) const
        {
            // Formatter can't access Product's `this` pointer
            // so we can't print m_name and m_price directly
            // Instead, we have to pass in a Product object to use
            // Because Formatter is a member of Product,
            // we can access private members product.m_name and product.m_price directly
            std::cout << product.m_name << " costs: $" << product.m_price << '\n';
        }
    };

private:
    std::string m_name{};
    PriceType m_price{};
    int m_quantity{};

public:
    Product(std::string_view name, PriceType price, int quantity)
        : m_name{ name }
        , m_price{ price }
        , m_quantity{ quantity }
    {
    }

    // removed the accessor functions in this example (since they aren't used)
};

int main()
{
    const Product item{ "Widget", 19.99, 100 };
    const Product::Formatter formatter{}; // instantiate an object of the inner class
    formatter.print(item);

    return 0;
}

This prints:

Widget costs: $19.99

There is one case where nested classes are more commonly used. In the standard library, most iterator classes are implemented as nested classes of the container they're designed to iterate over. For example, std::string::iterator is implemented as a nested class of std::string. We'll cover iterators in a future chapter.

Nested types and forward declarations

A nested type can be forward declared within the class that encloses it. The nested type can then be defined later, either within the enclosing class or outside of it. For example:

#include <iostream>

class Container
{
public:
    class Element;   // okay: forward declaration inside the enclosing class
    class Element{}; // okay: definition of forward declared type inside the enclosing class
    class Data;      // okay: forward declaration inside the enclosing class
};

class Container::Data // okay: definition of forward declared type outside the enclosing class
{
};

int main()
{
    return 0;
}

However, a nested type cannot be forward declared prior to the definition of the enclosing class.

#include <iostream>

class Container;           // okay: can forward declare non-nested type
class Container::Element;  // error: can't forward declare nested type prior to outer class definition

class Container
{
public:
    class Element{}; // note: nested type declared here
};

class Container::Element;  // okay (but redundant) since nested type has already been declared as part of Container class definition

int main()
{
    return 0;
}

While you can forward declare a nested type after the definition of the enclosing class, since the enclosing class will already contain a declaration for the nested type, doing so is redundant.

Summary

Nested types (member types): Class types can contain type definitions as members, including enumerations, type aliases, and even other classes. Define nested types under the appropriate access specifier, typically at the top of the class definition.

Showing relationships: Nested types clarify that a type is specifically associated with and used by the containing class, making the code's intent more explicit than having separate global types.

Scope and access: Nested type names must be fully qualified when used outside the class (e.g., Drawable::ShapeType). Within class members, the scope qualifier is optional. Nested types follow normal access rules based on their access specifier.

Unscoped enums as nested types: When nesting unscoped enumerations, the class itself provides scoping, so enumerators are accessed as ClassName::enumerator rather than ClassName::EnumName::enumerator. This eliminates the need for scoped enumerations in this context.

Nested type aliases: Classes commonly define type aliases using using (e.g., using PriceType = double;). Standard library classes make extensive use of nested typedefs - std::string alone defines ten nested type aliases.

Nested classes: Classes can contain other classes as nested types, though this is less common. Nested classes cannot access the outer class's this pointer but can access private members of outer class objects passed to them as parameters.

Iterator pattern: Nested classes are commonly used for iterators in the standard library. For example, std::string::iterator is implemented as a nested class of std::string.

Forward declaration rules: Nested types can be forward declared within the enclosing class and defined later (either inside or outside the class). However, nested types cannot be forward declared before the enclosing class is defined.

Best practice - define at top: Define nested types at the beginning of your class definition since they must be fully defined before they can be used by other class members.

Nested types are a powerful organizational tool that helps create self-contained, well-structured class interfaces while keeping related types together in a logical hierarchy.