W poprzedniej lekcji 26.4 -- Specjalizacja szablonu zajęć, przyjrzeliśmy się prostej klasie templated Storage wraz ze specjalizacją dla typu double:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
template<>
void Storage<double>::print() // w pełni wyspecjalizowana dla typu double
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
// Zdefiniuj niektóre jednostki przechowywania
Storage i { 5 };
Storage d { 6.7 }; // spowoduje niejawne utworzenie instancji Storage<double>
// Wydrukuj niektóre wartości
i.print(); // wywołuje Storage<int>::print (utworzone z Storage<T>)
d.print(); // wywołuje Storage<double>::print (wywoływany z jawnej specjalizacji Storage<double>::print())
}Jednak choć ta klasa jest prosta, ma ukrytą wadę: kompiluje się ale działa nieprawidłowo, gdy T jest typem wskaźnika. Na przykład:
int main()
{
double d { 1.2 };
double *ptr { &d };
Storage s { ptr };
s.print();
return 0;
}Na komputerze autora dało to wynik:
0x7ffe164e0f50
Co się stało? Ponieważ ptr jest double*, s ma typ Storage<double*>, czyli m_value ma typ double*. Po wywołaniu konstruktora m_value otrzymuje kopię adresu, który ptr przetrzymuje i to właśnie ten adres jest drukowany, gdy wywoływana jest funkcja składowa print() .
Jak więc to naprawić?
Jedną z opcji byłoby dodanie pełnej specjalizacji typu double*:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
template<>
void Storage<double*>::print() // w pełni wyspecjalizowana dla typu double*
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}
template<>
void Storage<double>::print() // w pełni wyspecjalizowana dla typu double (dla porównania, nieużywana)
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
double d { 1.2 };
double *ptr { &d };
Storage s { ptr };
s.print(); // wywołuje Storage<double**>::print()
return 0;
}To teraz wypisuje prawidłowy wynik:
1.200000e+00
Ale to rozwiązuje problem tylko wtedy, gdy T jest typu double*. A co jeśli T Jest int* lub char* lub jakikolwiek inny typ wskaźnika?
Naprawdę nie chcemy tworzyć pełnej specjalizacji dla każdego typu wskaźnika. I tak naprawdę nie jest to nawet możliwe, ponieważ użytkownik zawsze może przekazać wskaźnik do typu zdefiniowanego w programie.
Częściowa specjalizacja szablonu dla wskaźników
Możesz pomyśleć o utworzeniu funkcji szablonowej przeciążonej na typie T*:
// doesn't work
template<typename T>
void Storage<T*>::print()
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}Taka funkcja jest częściowo wyspecjalizowaną funkcją szablonową, ponieważ ogranicza jaki typ T może być (do typu wskaźnikowego), ale T jest nadal szablonem typu parametr.
Niestety to nie działa z prostego powodu: w chwili pisania tego tekstu (C++23) funkcje nie mogą być częściowo wyspecjalizowane. Jak zauważyliśmy w lekcji 26.5 — Częściowa specjalizacja szablonów, tylko klasy mogą być częściowo specjalizowane.
Więc zamiast tego specjalizujmy się częściowo Storage klasę:
#include <iostream>
template <typename T>
class Storage // To jest nasza główna klasa szablonów (taka sama jak poprzednia)
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
template <typename T> // nadal mamy parametr szablonu typu
class Storage<T*> // To jest częściowo wyspecjalizowane dla T*
{
private:
T* m_value {};
public:
Storage(T* value)
: m_value { value }
{
}
void print();
};
template <typename T>
void Storage<T*>::print() // To jest niewyspecjalizowana funkcja częściowo wyspecjalizowanej klasy Storage<T*>
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}
int main()
{
double d { 1.2 };
double *ptr { &d };
Storage s { ptr }; // tworzy instancję Storage<double*> z częściowo wyspecjalizowanej klasy
s.print(); // wywołuje Storage<double**>::print()
return 0;
}Zdefiniowaliśmy Storage<T*>::print() poza klasą tylko po to, aby pokazać, jak to się robi i pokazać, że definicja jest identyczna z częściowo wyspecjalizowaną funkcja Storage<T*>::print() która nie działała powyżej. Jednakże teraz, gdy Storage<T*> jest klasą częściowo wyspecjalizowaną, Storage<T*>::print() nie jest już częściowo wyspecjalizowana — jest to funkcja niewyspecjalizowana i dlatego jest dozwolona.
Warto zauważyć, że nasz parametr szablonu typu jest zdefiniowany jako T, nie T*. Oznacza to, że T zostanie wydedukowany jako typ niewskaźnikowy, więc musimy użyć T* wszędzie tam, gdzie chcemy, aby wskaźnik był T. Warto również przypomnieć, że częściową specjalizację Storage<T*> należy zdefiniować po podstawowej klasie szablonu Storage<T>.
Kwestie własności i czasu życia
Powyższa częściowo wyspecjalizowana klasa Storage<T*> ma inny potencjalny problem. Ponieważ m_value jest T* jest to wskaźnik do obiektu, który jest przekazywany. Jeśli obiekt ten zostanie następnie zniszczony, naszym Storage<T*> pozostaną wiszące.
głównym problemem jest to, że nasza implementacja Storage<T> ma semantykę kopiowania (co oznacza, że tworzy kopię swojego inicjatora), ale Storage<T*> ma semantykę referencji (co oznacza, że jest referencją do swojego inicjatora). Ta niespójność jest przepisem na błędy.
Istnieje kilka różnych sposobów radzenia sobie z takimi problemami (w kolejności rosnącej złożoności):
- Wyjaśnij, naprawdę jasno, że
Storage<T*>jest klasą widoku (z semantyką referencyjną), więc to wywołujący musi upewnić się, że wskazywany obiekt pozostanie ważny tak długo, jak robi toStorage<T*>. Niestety, ponieważ ta częściowo wyspecjalizowana klasa musi mieć taką samą nazwę jak podstawowa klasa szablonowa, nie możemy nadać jej nazwy typuStorageView. Dlatego jesteśmy ograniczeni do używania komentarzy i innych rzeczy, które mogą zostać pominięte. Niezbyt dobra opcja. - Zapobiegaj używaniu
Storage<T*>całkowicie. Prawdopodobnie nie potrzebujemyStorage<T*>istnieć, ponieważ osoba wywołująca może zawsze wyłuskać wskaźnik w momencie tworzenia instancji, aby użyćStorage<T>i utworzyć kopię wartości (która jest semantycznie odpowiednia dla klasy pamięci).
Jednak chociaż możesz usunąć przeciążoną funkcję, C++ (od C++ 23) nie pozwoli ci usunąć klasy. Oczywistym rozwiązaniem jest częściowa specjalizacja Storage<T*> , a następnie zrobienie czegoś, aby się nie skompilował (np. static_assert) po utworzeniu instancji szablonu. To podejście ma jedną poważną wadę: std::nullptr_t nie jest typem wskaźnika, więc Storage<std::nullptr_t> nie będzie pasować Storage<T*>!
Lepszym rozwiązaniem jest całkowite uniknięcie częściowej specjalizacji i użycie static_assert na naszym głównym szablonie aby upewnić się, że T jest typem, z którym się zgadzamy. Oto przykład takiego podejścia:
#include <iostream>
#include <type_traits> // dla std::is_pointer_v i std::is_null_pointer_v
template <typename T>
class Storage
{
// Upewnij się, że T nie jest wskaźnikiem ani a std::nullptr_t
static_assert(!std::is_pointer_v<T> && !std::is_null_pointer_v<T>, "Storage<T*> and Storage<nullptr> disallowed");
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
int main()
{
double d { 1.2 };
Storage s1 { d }; // ok
s1.print();
Storage s2 { &d }; // static_assert, ponieważ T jest wskaźnikiem
s2.print();
Storage s3 { nullptr }; // static_assert, ponieważ T jest nullptr
s3.print();
return 0;
}- Zwróć
Storage<T*>wykonaj kopię obiektu na stercie. Jeśli sam zarządzasz całą pamięcią sterty, wymaga to przeciążenia konstruktora, konstruktora kopiującego, przypisania kopii i destruktora. Łatwiejszą alternatywą jest po prostu użyciestd::unique_ptr(które omówimy w lekcji 22.5 — std::unique_ptr):
#include <iostream>
#include <type_traits> // dla std::is_pointer_v i std::is_null_pointer_v
#include <memory>
template <typename T>
class Storage
{
// Upewnij się, że T nie jest wskaźnikiem ani a std::nullptr_t
static_assert(!std::is_pointer_v<T> && !std::is_null_pointer_v<T>, "Storage<T*> and Storage<nullptr> disallowed");
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
template <typename T>
class Storage<T*>
{
private:
std::unique_ptr<T> m_value {}; // użyj std::unique_ptr, aby automatycznie zwolnić alokację w przypadku zniszczenia pamięci
public:
Storage(T* value)
: m_value { std::make_unique<T>(value ? *value : 0) } // lub zgłoś wyjątek, gdy !wartość
{
}
void print()
{
if (m_value)
std::cout << *m_value << '\n';
}
};
int main()
{
double d { 1.2 };
Storage s1 { d }; // ok
s1.print();
Storage s2 { &d }; // ok, kopiuje d na stertę
s2.print();
return 0;
}Korzystanie z częściowej specjalizacji klasy szablonowej do tworzenia oddzielnych implementacji klasy ze wskaźnikiem i bez wskaźnika jest niezwykle przydatne, gdy chcesz, aby klasa obsługiwała obydwa odmiennie, ale w sposób całkowicie przejrzysty dla użytkownika końcowego.

