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 is the expression parameter
class StaticArray
{
private:
    // The expression parameter controls the size of the array
    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> // size is a template non-type parameter
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()
{
	// declare an int array
	StaticArray<int, 4> int4{};
	int4[0] = 0;
	int4[1] = 1;
	int4[2] = 2;
	int4[3] = 3;

	// Print the array
	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()
{
    // Declare a char array
    StaticArray<char, 14> char14{};

    // Copy some data into it
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // Print the array
    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 is the expression parameter
class StaticArray
{
private:
	// The expression parameter controls the size of the array
	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] << ' ';
}

// Override print() for fully specialized StaticArray<char, 14>
template <>
void print(const StaticArray<char, 14>& array)
{
	for (int count{ 0 }; count < 14; ++count)
		std::cout << array[count];
}

int main()
{
    // Declare a char array
    StaticArray<char, 14> char14{};

    // Copy some data into it
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // Print the array
    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()
{
    // Declare a char array
    StaticArray<char, 12> char12{};

    // Copy some data into it
    constexpr std::string_view hello{ "Hello, mom!" };
    std::copy_n(hello.begin(), hello.size(), char12.getArray());

    // Print the array
    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:

// overload of print() function for partially specialized StaticArray<char, size>
template <int size> // size is still a template non-type parameter
void print(const StaticArray<char, size>& array) // we're explicitly defining type char here
{
	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 is the expression parameter
class StaticArray
{
private:
	// The expression parameter controls the size of the array
	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] << ' ';
}

// overload of print() function for partially specialized 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()
{
	// Declare an char array of size 14
	StaticArray<char, 14> char14{};

	// Copy some data into it
	constexpr std::string_view hello14{ "Hello, world!" };
	std::copy_n(hello14.begin(), hello14.size(), char14.getArray());

	// Print the array
	print(char14);

	std::cout << ' ';

	// Now declare an char array of size 12
	StaticArray<char, 12> char12{};

	// Copy some data into it
	constexpr std::string_view hello12{ "Hello, mom!" };
	std::copy_n(hello12.begin(), hello12.size(), char12.getArray());

	// Print the array
	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:

// Doesn't work, can't partially specialize functions
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';
}

// Partially specialized class
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;
};

// Member function of partially specialized class
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()
{
	// declare an integer array with room for 6 integers
	StaticArray<int, 6> intArray{};

	// Fill it up in order, then print it
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// declare a double buffer with room for 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 is the expression parameter
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';
	}

	// Don't forget a virtual destructor if you're going to use virtual function resolution
};

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] << ' ';
// note: The this-> prefix in the above line is needed.
// 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()
{
	// declare an integer array with room for 6 integers
	StaticArray<int, 6> intArray{};

	// Fill it up in order, then print it
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// declare a double buffer with room for 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