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() // fully specialized for type double
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
// Define some storage units
Storage i { 5 };
Storage d { 6.7 }; // will cause Storage<double> to be implicitly instantiated
// Print out some values
i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
d.print(); // calls Storage<double>::print (called from explicit specialization of 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() // fully specialized for type double*
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}
template<>
void Storage<double>::print() // fully specialized for type double (for comparison, not used)
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
double d { 1.2 };
double *ptr { &d };
Storage s { ptr };
s.print(); // calls 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 // This is our primary template class (same as previous)
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
template <typename T> // we still have a type template parameter
class Storage<T*> // This is partially specialized for T*
{
private:
T* m_value {};
public:
Storage(T* value)
: m_value { value }
{
}
void print();
};
template <typename T>
void Storage<T*>::print() // This is a non-specialized function of partially specialized class Storage<T*>
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}
int main()
{
double d { 1.2 };
double *ptr { &d };
Storage s { ptr }; // instantiates Storage<double*> from partially specialized class
s.print(); // calls 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 dzwonią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> // for std::is_pointer_v and std::is_null_pointer_v
template <typename T>
class Storage
{
// Make sure T isn't a pointer or 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 because T is a pointer
s2.print();
Storage s3 { nullptr }; // static_assert because T is a 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> // for std::is_pointer_v and std::is_null_pointer_v
#include <memory>
template <typename T>
class Storage
{
// Make sure T isn't a pointer or 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 {}; // use std::unique_ptr to automatically deallocate when Storage is destroyed
public:
Storage(T* value)
: m_value { std::make_unique<T>(value ? *value : 0) } // or throw exception when !value
{
}
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, copies d on heap
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.

