15,3 — Typy zagnieżdżone (typy składowe)

Rozważ następujący krótki program:

#include <iostream>

enum class FruitType
{
	apple,
	banana,
	cherry
};

class Fruit
{
private:
	FruitType m_type { };
	int m_percentageEaten { 0 };

public:
	Fruit(FruitType type) :
		m_type { type }
	{
	}

	FruitType getType() { return m_type; }
	int getPercentageEaten() { return m_percentageEaten; }

	bool isCherry() { return m_type == FruitType::cherry; }

};

int main()
{
	Fruit apple { FruitType::apple };
	
	if (apple.getType() == FruitType::apple)
		std::cout << "I am an apple";
	else
		std::cout << "I am not an apple";
	
	return 0;
}

W tym programie nie ma nic złego. Ponieważ jednak klasa enum class FruitType ma być używana w połączeniu z klasą Fruit , istnienie jej niezależnie od klasy pozostawia nam możliwość wywnioskowania, w jaki sposób są one połączone.

Typy zagnieżdżone (typy składowe)

Jak dotąd widzieliśmy typy klas z dwoma różnymi rodzajami elementów członkowskich: członkami danych i funkcjami składowymi. Nasza Fruit klasa w powyższym przykładzie zawiera oba te elementy.

Typy klas obsługują inny rodzaj elementów: typy zagnieżdżone (tzw typy elementów). Aby utworzyć typ zagnieżdżony, wystarczy zdefiniować typ wewnątrz klasy, pod odpowiednim specyfikatorem dostępu.

Oto ten sam program, co powyżej, przepisany tak, aby korzystał z typu zagnieżdżonego zdefiniowanego w Fruit klasę:

#include <iostream>

class Fruit
{
public:
	// FruitType has been moved inside the class, under the public access specifier
        // We've also renamed it Type and made it an enum rather than an enum class
	enum Type
	{
		apple,
		banana,
		cherry
	};

private:
	Type m_type {};
	int m_percentageEaten { 0 };

public:
	Fruit(Type type) :
		m_type { type }
	{
	}

	Type getType() { return m_type;  }
	int getPercentageEaten() { return m_percentageEaten;  }

	bool isCherry() { return m_type == cherry; } // Inside members of Fruit, we no longer need to prefix enumerators with FruitType::
};

int main()
{
	// Note: Outside the class, we access the enumerators via the Fruit:: prefix now
	Fruit apple { Fruit::apple };
	
	if (apple.getType() == Fruit::apple)
		std::cout << "I am an apple";
	else
		std::cout << "I am not an apple";
	
	return 0;
}

Jest kilka rzeczy, na które warto zwrócić uwagę.

Najpierw zauważ, że FruitType jest teraz zdefiniowany wewnątrz klasy, gdzie został zmieniono nazwę Type z powodów, które wkrótce omówimy.

Drugi, zagnieżdżony typ Type został zdefiniowany na górze klasy. Nazwy typów zagnieżdżonych muszą być w pełni zdefiniowane, zanim będzie można ich użyć, dlatego zwykle są definiowane jako pierwsze.

Najlepsza praktyka

Zdefiniuj dowolne typy zagnieżdżone na górze typu klasy.

Po trzecie, typy zagnieżdżone podlegają normalnym regułom dostępu. Type jest definiowany w public specyfikatorze dostępu, dzięki czemu nazwa typu i moduły wyliczające mogą być bezpośrednio dostępne dla public.

Po czwarte, typy klas pełnią rolę obszaru zasięgu dla nazw zadeklarowanych wewnątrz nich, tak samo jak robią to przestrzenie nazw. Dlatego w pełni kwalifikowana nazwa Type Jest Fruit::Type i w pełni kwalifikowana nazwa apple enumeratora to Fruit::apple.

W obrębie elementów klasy nie musimy używać w pełni kwalifikowanej nazwy. Na przykład w funkcji składowej isCherry() uzyskujemy dostęp do modułu wyliczającego cherry bez Fruit:: kwalifikatora zakresu.

Poza klasą musimy używać pełnej nazwy (np. Fruit::apple). Zmieniliśmy nazwę FruitType Do Type abyśmy mogli uzyskać do niej dostęp jako Fruit::Type (zamiast bardziej nadmiarowego Fruit::FruitType).

Na koniec zmieniliśmy nasz typ wyliczeniowy z o zasięgu na bez zakresu. Ponieważ sama klasa działa teraz jako region zasięgu, używanie również modułu wyliczającego o zasięgu jest w pewnym stopniu zbędne. Zmiana na wyliczenie bez zakresu oznacza, że możemy uzyskać dostęp do modułów wyliczających ponieważ Fruit::apple zamiast dłuższych Fruit::Type::apple musielibyśmy użyć, gdyby moduł wyliczający miał zakres.

Zagnieżdżone definicje typów i aliasy typów

Typy klas mogą również zawierać zagnieżdżone definicje typów lub aliasy typów:

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

class Employee
{
public:
    using IDType = int;

private:
    std::string m_name{};
    IDType m_id{};
    double m_wage{};

public:
    Employee(std::string_view name, IDType id, double wage)
        : m_name { name }
        , m_id { id }
        , m_wage { wage }
    {
    }

    const std::string& getName() { return m_name; }
    IDType getId() { return m_id; } // can use unqualified name within class
};

int main()
{
    Employee john { "John", 1, 45000 };
    Employee::IDType id { john.getId() }; // must use fully qualified name outside class

    std::cout << john.getName() << " has id: " << id << '\n';

    return 0;
}

Wypisuje:

John has id: 1

Zauważ, że wewnątrz klasy możemy po prostu używać IDType, ale poza klasy musimy użyć w pełni kwalifikowanej nazwy Employee::IDType.

Zalety aliasów typów omawiamy w lekcji 10.7 -- Typedefs i aliasy typów, a tutaj służą temu samemu celowi. Bardzo często klasy w standardowej bibliotece C++ korzystają z zagnieżdżonych deflektorów typów. W chwili pisania tego tekstu std::string definiuje dziesięć zagnieżdżonych deflektorów typów!

Klasy zagnieżdżone i dostęp do klasy zewnętrznej. członkowie

Dość rzadko zdarza się, aby klasy miały inne klasy jako typ zagnieżdżony, ale jest to możliwe. W C++ klasa zagnieżdżona nie ma dostępu do this wskaźnika klasy zewnętrznej (zawierającej), więc klasy zagnieżdżone nie mają bezpośredniego dostępu do elementów klasy zewnętrznej. Dzieje się tak, ponieważ instancja klasy zagnieżdżonej może zostać utworzona niezależnie od klasy zewnętrznej (w takim przypadku nie byłoby żadnych elementów klasy zewnętrznej). dostęp!)

Jednakże, ponieważ klasy zagnieżdżone są członkami klasy zewnętrznej, mogą uzyskać dostęp do dowolnych prywatnych elementów klasy zewnętrznej znajdujących się w zasięgu.

Zilustrujmy to przykładem:

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

class Employee
{
public:
    using IDType = int;

    class Printer
    {
    public:
        void print(const Employee& e) const
        {
            // Printer can't access Employee's `this` pointer
            // so we can't print m_name and m_id directly
            // Instead, we have to pass in an Employee object to use
            // Because Printer is a member of Employee,
            // we can access private members e.m_name and e.m_id directly
            std::cout << e.m_name << " has id: " << e.m_id << '\n';
        }
    };

private:
    std::string m_name{};
    IDType m_id{};
    double m_wage{};

public:
    Employee(std::string_view name, IDType id, double wage)
        : m_name{ name }
        , m_id{ id }
        , m_wage{ wage }
    {
    }

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

int main()
{
    const Employee john{ "John", 1, 45000 };
    const Employee::Printer p{}; // instantiate an object of the inner class
    p.print(john);

    return 0;
}

Wypisuje:

John has id: 1

Jest jeden przypadek, w którym klasy zagnieżdżone są częściej używane w bibliotece standardowej, większość klas iteratorów jest implementowanych jako klasy zagnieżdżone kontenera, po którym mają iterować przykład std::string::iterator jest zaimplementowany jako klasa zagnieżdżona std::string. Iteratorami zajmiemy się w następnym rozdziale.

Typy zagnieżdżone i deklaracje forward

Typ zagnieżdżony można zadeklarować do przodu w klasie, która go otacza. Typ zagnieżdżony można następnie zdefiniować później w klasie otaczającej lub poza nią. Na przykład:

#include <iostream>

class outer
{
public:
    class inner1;   // okay: forward declaration inside the enclosing class okay
    class inner1{}; // okay: definition of forward declared type inside the enclosing class
    class inner2;   // okay: forward declaration inside the enclosing class okay
};

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

int main()
{
    return 0;
}

Jednakże typu zagnieżdżonego nie można zadeklarować w przód przed definicją otaczającej klasy.

#include <iostream>

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

class outer
{
public:
    class inner1{}; // note: nested type declared here
};

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

int main()
{
    return 0;
}

Chociaż możesz zadeklarować typ zagnieżdżony po zdefiniowaniu klasy otaczającej, ponieważ klasa otaczająca będzie już zawierać deklarację dla typu zagnieżdżonego, robienie tego jest zbędne.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
95 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze