26.5 — Częściowa specjalizacja szablonów

Ta i następne lekcje są lekturą opcjonalną dla osób pragnących głębszej wiedzy o szablonach C++. Częściowa specjalizacja szablonów nie jest używana zbyt często (ale może być użyteczna w określonych przypadkach).

W lekcji 26.2 -- Parametry nietypowe szablonu, wiesz, jak parametry wyrażeń mogą być używane do parametryzacji klas szablonów.

Przyjrzyjmy się jeszcze raz klasie Static Array, której użyliśmy w jednym z naszych poprzednich przykładów:

template <typename T, int size> // size to parametr wyrażenia
class StaticArray
{
private:
    // Parametr wyrażenia steruje rozmiarem tablicy
    T m_array[size]{};
 
public:
    T* getArray() { return m_array; }
	
    const T& operator[](int index) const { return m_array[index]; }
    T& operator[](int index) { return m_array[index]; }
};

Ta klasa przyjmuje dwa parametry szablonu: jeden parametr typu i jedno wyrażenie parametr.

Załóżmy teraz, że chcemy napisać funkcję wyświetlającą całą tablicę. Chociaż moglibyśmy zaimplementować tę funkcję jako funkcję składową, zrobimy to zamiast niej jako funkcję niebędącą składową, ponieważ ułatwi to śledzenie kolejnych przykładów.

Korzystając z szablonów, moglibyśmy napisać coś takiego:

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
    for (int count{ 0 }; count < size; ++count)
        std::cout << array[count] << ' ';
}

Umożliwiłoby nam to wykonanie następujących czynności:

#include <iostream>

template <typename T, int size> // rozmiar do parametru alternatywnego szablonu
class StaticArray
{
private:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

int main()
{
	// zadeklaruj tablicę int
	StaticArray<int, 4> int4{};
	int4[0] = 0;
	int4[1] = 1;
	int4[2] = 2;
	int4[3] = 3;

	// Wydrukuj tablicę
	print(int4);

	return 0;
}

i otrzymanie następującego wyniku:

0 1 2 3

Chociaż to działa, ma wadę projektową. Rozważ następujące kwestie:

#include <algorithm>
#include <iostream>
#include <string_view>

int main()
{
    // Zadeklaruj tablicę char
    StaticArray<char, 14> char14{};

    // Kopiuj do niej część danych
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // Wydrukuj tablicę
    print(char14);

    return 0;
}

(Omówiliśmy std::strcpy w lekcji 17.10 -- Ciągi znaków w stylu C jeśli potrzebujesz odświeżenia wiedzy)

Ten program skompiluje, wykona i wygeneruje następującą wartość (lub podobną):

H e l l o ,   w o r l d !

W przypadku typów innych niż znakowe sensowne jest wstawienie spacji pomiędzy każdym elementem tablicy, aby nie działały razem. Jednakże w przypadku typu char rozsądniej jest wydrukować wszystko razem jako ciąg znaków w stylu C, czego nie robi nasza funkcja print().

Jak więc możemy to naprawić?

Specjalizacja szablonów na ratunek?

Najpierw można pomyśleć o zastosowaniu specjalizacji szablonów. Problem z pełną specjalizacją szablonu polega na tym, że wszystkie parametry szablonu muszą być jawnie zdefiniowane.

Rozważ:

#include <algorithm>
#include <iostream>
#include <string_view>

template <typename T, int size> // size to parametr wyrażenia
class StaticArray
{
private:
	// Parametr wyrażenia steruje rozmiarem tablicy
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

// Zastąp funkcję print() dla w pełni wyspecjalizowanej tablicy StaticArray<char, 14>
template <>
void print(const StaticArray<char, 14>& array)
{
	for (int count{ 0 }; count < 14; ++count)
		std::cout << array[count];
}

int main()
{
    // Zadeklaruj tablicę char
    StaticArray<char, 14> char14{};

    // Kopiuj do niej część danych
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // Wydrukuj tablicę
    print(char14);

    return 0;
}

Jak widać, udostępniliśmy teraz przeciążoną funkcję drukowania dla w pełni specjalizowanego StaticArray<char, 14>. Rzeczywiście, to wypisuje:

Hello, world!

Chociaż rozwiązuje to problem upewnienia się, że print() można wywołać za pomocą StaticArray<char, 14>, pojawia się inny problem: użycie pełnej specjalizacji szablonu oznacza, że ​​musimy jawnie zdefiniować długość tablicy, którą ta funkcja zaakceptuje! Rozważmy następujący przykład:

int main()
{
    // Zadeklaruj tablicę char
    StaticArray<char, 12> char12{};

    // Kopiuj do niej część danych
    constexpr std::string_view hello{ "Hello, mom!" };
    std::copy_n(hello.begin(), hello.size(), char12.getArray());

    // Wydrukuj tablicę
    print(char12);

    return 0;
}

Wywołanie print() z char12 wywoła wersję print() która wymaga StaticArray<T, size>, ponieważ char12 jest typu StaticArray<char, 12>, a nasza przeciążona funkcja print() zostanie wywołana tylko po przekazaniu StaticArray<char, 14>.

Chociaż moglibyśmy utworzyć kopię print() obsługującą StaticArray<char, 12>, co się stanie, gdy będziemy chcieli wywołać print() z tablicą o rozmiarze 5 czy 22? Musielibyśmy skopiować funkcję dla każdego innego rozmiaru tablicy. To zbędne.

Oczywiście pełna specjalizacja szablonów jest tutaj zbyt restrykcyjnym rozwiązaniem. Rozwiązaniem, którego szukamy jest częściowa specjalizacja szablonów.

Częściowa specjalizacja szablonu

Częściowa specjalizacja szablonów pozwala nam na specjalizację klas (ale nie poszczególnych funkcji!), w których niektóre, ale nie wszystkie parametry szablonu zostały jawnie zdefiniowane. W przypadku powyższego wyzwania idealnym rozwiązaniem byłoby, gdyby nasza przeciążona funkcja drukowania działała ze StaticArray typu char, ale pozostawiłaby szablon wyrażenia długości, aby mógł się zmieniać w razie potrzeby. Częściowa specjalizacja szablonów właśnie to nam pozwala!

Oto nasz przykład z przeciążoną funkcją drukowania, która przyjmuje częściowo wyspecjalizowaną tablicę StaticArray:

// przeciążenie funkcji print() dla częściowo wyspecjalizowanej StaticArray<char, size>
template <int size> // size jest nadal parametrem innym niż typ szablonu
void print(const StaticArray<char, size>& array) // jawnie określmy typ char tutaj
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count];
}

Jak widać tutaj, wyraźnie zadeklarowaliśmy, że ta funkcja będzie działać tylko dla StaticArray typu char, ale size jest nadal parametrem wyrażenia szablonowym, więc będzie działać dla tablic char o dowolnym rozmiarze. To wszystko!

Oto pełny program wykorzystujący to:

#include <algorithm>
#include <iostream>
#include <string_view>

template <typename T, int size> // size to parametr wyrażenia
class StaticArray
{
private:
	// Parametr wyrażenia steruje rozmiarem tablicy
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

// przeciążenie funkcji print() dla częściowo wyspecjalizowanej StaticArray<char, size>
template <int size>
void print(const StaticArray<char, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count];
}

int main()
{
	// Zadeklaruj tablicę znaków o rozmiarze 14
	StaticArray<char, 14> char14{};

	// Kopiuj do niej część danych
	constexpr std::string_view hello14{ "Hello, world!" };
	std::copy_n(hello14.begin(), hello14.size(), char14.getArray());

	// Wydrukuj tablicę
	print(char14);

	std::cout << ' ';

	// Teraz zadeklaruj tablicę znaków o rozmiarze 12
	StaticArray<char, 12> char12{};

	// Kopiuj do niej część danych
	constexpr std::string_view hello12{ "Hello, mom!" };
	std::copy_n(hello12.begin(), hello12.size(), char12.getArray());

	// Wydrukuj tablicę
	print(char12);

	return 0;
}

Wypisuje:

Hello, world! Hello, mom!

Tak jak się spodziewamy.

Częściowa specjalizacja szablonów może być używana tylko z klasami, a nie z funkcjami szablonów (funkcje muszą być w pełni wyspecjalizowane). Nasz void print(StaticArray<char, size> &array) przykład działa, ponieważ funkcja print nie jest częściowo wyspecjalizowana (jest to po prostu przeciążona funkcja szablonowa, która tak się składa, że ​​ma częściowo wyspecjalizowany parametr klasy).

Częściowa specjalizacja szablonu dla funkcji składowych

Ograniczenie częściowej specjalizacji funkcji może prowadzić do pewnych wyzwań podczas pracy z funkcjami składowymi. Na przykład, co by było, gdybyśmy zdefiniowali StaticArray w ten sposób?

template <typename T, int size>
class StaticArray
{
private:
    T m_array[size]{};
 
public:
    T* getArray() { return m_array; }
	
    const T& operator[](int index) const { return m_array[index]; }
    T& operator[](int index) { return m_array[index]; }

    void print() const;
};

template <typename T, int size> 
void StaticArray<T, size>::print() const
{
    for (int i{ 0 }; i < size; ++i)
        std::cout << m_array[i] << ' ';
    std::cout << '\n';
}

print() jest teraz funkcją składową klasy StaticArray<T, int>. Co więc się stanie, gdy chcemy częściowo wyspecjalizować funkcję print(), aby działała inaczej? Możesz spróbować tego:

// Nie działa, nie można częściowo wyspecjalizować funkcji
template <int size>
void StaticArray<double, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << std::scientific << m_array[i] << ' ';
	std::cout << '\n';
}

Niestety to nie działa, ponieważ próbujemy częściowo wyspecjalizować funkcję, co jest niedozwolone.

Jak więc obejść ten problem? Jednym z oczywistych sposobów jest częściowa specjalizacja całej klasy:

#include <iostream>

template <typename T, int size>
class StaticArray
{
private:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }

	void print() const;
};

template <typename T, int size> 
void StaticArray<T, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << m_array[i] << ' ';
	std::cout << '\n';
}

// Zajęcia częściowo specjalistyczne
template <int size>
class StaticArray<double, size>
{
private:
	double m_array[size]{};

public:
	double* getArray() { return m_array; }

	const double& operator[](int index) const { return m_array[index]; }
	double& operator[](int index) { return m_array[index]; }

	void print() const;
};

// Funkcja składowa częściowo wyspecjalizowanej klasy
template <int size>
void StaticArray<double, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << std::scientific << m_array[i] << ' ';
	std::cout << '\n';
}

int main()
{
	// zadeklaruj tablicę liczb całkowitych z miejscem na 6 liczb całkowitych
	StaticArray<int, 6> intArray{};

	// Wypełnij w kolejności, a następnie wydrukuj
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// deklaracja podwójny bufor z miejscem na 4 doubles
	StaticArray<double, 4> doubleArray{};

	for (int count{ 0 }; count < 4; ++count)
		doubleArray[count] = (4.0 + 0.1 * count);

	doubleArray.print();

	return 0;
}

Wypisuje:

0 1 2 3 4 5
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00

Działa to, ponieważ StaticArray<double, size>::print() nie jest już częściowo wyspecjalizowaną funkcją - jest niewyspecjalizowanym członkiem częściowo wyspecjalizowanej klasy StaticArray<double, size>.

Nie jest to jednak świetne rozwiązanie, ponieważ musimy zduplikować dużo kodu z StaticArray<T, size> Do StaticArray<double, size>.

Gdyby tylko istniał jakiś sposób na ponowne wykorzystanie kodu w StaticArray<T, size> w StaticArray<double, size>. Brzmi jak zadanie związane z dziedziczeniem!

Możesz zacząć od próby napisania tego kodu w ten sposób:

template <int size> // size to parametr wyrażenia
class StaticArray<double, size>: public StaticArray<T, size>

Ale to nie działa, ponieważ użyliśmy T bez jego definiowania. Nie ma składni, która pozwalałaby nam dziedziczyć w taki sposób.

Na marginesie…

Nawet gdybyśmy mogli zdefiniować T jako parametr szablonu typu, podczas tworzenia instancji StaticArray<double, size> kompilator musiałby zastąpić T w StaticArray<T, size> rzeczywistym typem. Jakiego rzeczywistego typu by użył? Jedynym typem, który ma sens, jest T=double, ale spowodowałoby to StaticArray<double, size> dziedziczenie po sobie!

Na szczęście istnieje obejście polegające na użyciu wspólnej klasy bazowej:

#include <iostream>

template <typename T, int size>
class StaticArray_Base
{
protected:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }

	void print() const
	{
		for (int i{ 0 }; i < size; ++i)
			std::cout << m_array[i] << ' ';
		std::cout << '\n';
	}

	// Nie zapomnij o wirtualnym destruktorze, jeśli masz zamiar używać wirtualnego rozpoznawania funkcji
};

template <typename T, int size>
class StaticArray: public StaticArray_Base<T, size>
{
};

template <int size>
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:

	void print() const
	{
		for (int i{ 0 }; i < size; ++i)
			std::cout << std::scientific << this->m_array[i] << ' ';
// uwaga: przedrostek this-> w powyższym wierszu jest potrzebny.
// See https://stackoverflow.com/a/6592617 or https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for more info on why.
		std::cout << '\n';
	}
};

int main()
{
	// zadeklaruj tablicę liczb całkowitych z miejscem na 6 liczb całkowitych
	StaticArray<int, 6> intArray{};

	// Wypełnij w kolejności, a następnie wydrukuj
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// deklaracja podwójny bufor z miejscem na 4 doubles
	StaticArray<double, 4> doubleArray{};

	for (int count{ 0 }; count < 4; ++count)
		doubleArray[count] = (4.0 + 0.1 * count);

	doubleArray.print();

	return 0;
}

Wypisuje to samo co powyżej, ale zawiera znacznie mniej zduplikowanych kodów.

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