26,6 — Częściowa specjalizacja szablonów dla wskaźników

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):

  1. 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 to Storage<T*> . Niestety, ponieważ ta częściowo wyspecjalizowana klasa musi mieć taką samą nazwę jak podstawowa klasa szablonowa, nie możemy nadać jej nazwy typu StorageView. Dlatego jesteśmy ograniczeni do używania komentarzy i innych rzeczy, które mogą zostać pominięte. Niezbyt dobra opcja.
  2. Zapobiegaj używaniu Storage<T*> całkowicie. Prawdopodobnie nie potrzebujemy Storage<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;
}
  1. 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życie std::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.

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:  
127 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze