17.3 — Przekazywanie i zwracanie std::array

Obiekt typu std::array można przekazać do funkcji, tak jak każdy inny obiekt. Oznacza to, że jeśli miniemy a std::array według wartości zostanie wykonana kosztowna kopia. Dlatego zazwyczaj przechodzimy std::array przez (const) odniesienie, aby uniknąć takich kopii.

Dzięki std::array, zarówno typ elementu, jak i długość tablicy są częścią informacji o typie obiektu. Dlatego też, gdy używamy std::array jako parametru funkcji, musimy jawnie określić zarówno typ elementu, jak i długość tablicy:

#include <array>
#include <iostream>

void passByRef(const std::array<int, 5>& arr) // we must explicitly specify <int, 5> here
{
    std::cout << arr[0] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // CTAD deduces type std::array<int, 5>
    passByRef(arr);

    return 0;
}

CTAD nie działa (obecnie) z parametrami funkcji, więc nie możemy po prostu określić std::array tutaj i pozwolić kompilatorowi wydedukować argumenty szablonu.

Używanie szablonów funkcji do przekazywania std::array różnych typów elementów lub długości

Aby napisać funkcję, która może zaakceptować std::array dowolny typ elementu i dowolną długość, możemy utworzyć szablon funkcji, który parametryzuje zarówno typ elementu, jak i długość naszego std::array, a następnie C++ użyje tego szablonu funkcji do utworzenia instancji rzeczywistych funkcji z rzeczywistymi typami i długościami.

Powiązana treść

Szablony funkcji omawiamy w lekcji 11.6 -- Szablony funkcji.

Ponieważ std::array jest zdefiniowany w następujący sposób to:

template<typename T, std::size_t N> // N is a non-type template parameter
struct array;

Możemy utworzyć szablon funkcji, który używa tej samej deklaracji parametrów szablonu:

#include <array>
#include <iostream>

template <typename T, std::size_t N> // note that this template parameter declaration matches the one for std::array
void passByRef(const std::array<T, N>& arr)
{
    static_assert(N != 0); // fail if this is a zero-length std::array

    std::cout << arr[0] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // use CTAD to infer std::array<int, 5>
    passByRef(arr);  // ok: compiler will instantiate passByRef(const std::array<int, 5>& arr)

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // use CTAD to infer std::array<int, 6>
    passByRef(arr2); // ok: compiler will instantiate passByRef(const std::array<int, 6>& arr)

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // use CTAD to infer std::array<double, 5>
    passByRef(arr3); // ok: compiler will instantiate passByRef(const std::array<double, 5>& arr)

    return 0;
}

W powyższym przykładzie utworzyliśmy szablon pojedynczej funkcji o nazwie passByRef() , którego parametr typu std::array<T, N>. T i N są zdefiniowane w deklaracji parametrów szablonu w poprzedniej linii: template <typename T, std::size_t N>. T jest parametrem szablonu typu standardowego, który pozwala wywołującemu określić element type. N to nietypowy parametr szablonu typu std::size_t który pozwala wywołującemu określić długość tablicy.

Ostrzeżenie

Zauważ, że typem nietypowego parametru szablonu dla std::array powinno być std::size_t, nie int! Dzieje się tak dlatego, że std::array definiuje się jako template<class T, std::size_t N> struct array;. Jeśli użyjesz int jako typu parametru szablonu innego niż typ, kompilator nie będzie w stanie dopasować argumentu typu std::array<T, std::size_t> z parametrem typu std::array<T, int> (a szablony nie wykonają konwersji).

Dlatego, gdy wywołamy passByRef(arr) z main() (gdzie arr jest zdefiniowane jako std::array<int, 5>), kompilator utworzy instancję i wywoła void passByRef(const std::array<int, 5>& arr). Podobny proces zachodzi w przypadku arr2 i arr3.

W ten sposób stworzyliśmy pojedynczy szablon funkcji, który może tworzyć instancje funkcji do obsługi std::array argumentów dowolnego typu i długości elementu!

W razie potrzeby możliwe jest również utworzenie szablonu tylko jednego z dwóch parametrów szablonu. W poniższym przykładzie parametryzujemy tylko długość std::array, ale typ elementu jest jawnie zdefiniowany jako int:

#include <array>
#include <iostream>

template <std::size_t N> // note: only the length has been templated here
void passByRef(const std::array<int, N>& arr) // we've defined the element type as int
{
    static_assert(N != 0); // fail if this is a zero-length std::array

    std::cout << arr[0] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // use CTAD to infer std::array<int, 5>
    passByRef(arr);  // ok: compiler will instantiate passByRef(const std::array<int, 5>& arr)

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // use CTAD to infer std::array<int, 6>
    passByRef(arr2); // ok: compiler will instantiate passByRef(const std::array<int, 6>& arr)

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // use CTAD to infer std::array<double, 5>
    passByRef(arr3); // error: compiler can't find matching function

    return 0;
}

Automatyczne parametry szablonu innego typu C++20

Konieczność zapamiętywania (lub sprawdzania) typu parametru szablonu innego niż typ, aby można było go użyć w deklaracji parametrów szablonu dla własnych szablonów funkcji, jest uciążliwa.

W C++20 możemy to zrobić użyj auto w deklaracji parametrów szablonu, aby parametr szablonu niebędący typem wywnioskował jego typ z argumentu:

#include <array>
#include <iostream>

template <typename T, auto N> // now using auto to deduce type of N
void passByRef(const std::array<T, N>& arr)
{
    static_assert(N != 0); // fail if this is a zero-length std::array

    std::cout << arr[0] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // use CTAD to infer std::array<int, 5>
    passByRef(arr);  // ok: compiler will instantiate passByRef(const std::array<int, 5>& arr)

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // use CTAD to infer std::array<int, 6>
    passByRef(arr2); // ok: compiler will instantiate passByRef(const std::array<int, 6>& arr)

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // use CTAD to infer std::array<double, 5>
    passByRef(arr3); // ok: compiler will instantiate passByRef(const std::array<double, 5>& arr)

    return 0;
}

Jeśli Twój kompilator obsługuje C++20, można tego użyć.

Statyczne potwierdzanie długości tablicy

Rozważ następującą funkcję szablonu, która jest podobna do tej przedstawionej powyżej:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void printElement3(const std::array<T, N>& arr)
{
    std::cout << arr[3] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr);

    return 0;
}

Chociaż printElement3() działa dobrze w tym przypadku, w tym programie na nieostrożnego programistę czeka potencjalny błąd. Widzisz to?

Powyższy program wypisuje wartość elementu tablicy o indeksie 3. Jest to w porządku, o ile tablica zawiera prawidłowy element o indeksie 3. Jednakże kompilator z radością pozwoli Ci przekazać tablice, w których indeks 3 jest poza dopuszczalnym zakresem. Na przykład:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void printElement3(const std::array<T, N>& arr)
{
    std::cout << arr[3] << '\n'; // invalid index
}

int main()
{
    std::array arr{ 9, 7 }; // a 2-element array (valid indexes 0 and 1)
    printElement3(arr);

    return 0;
}

Prowadzi to do niezdefiniowanego zachowania. Idealnie byłoby, gdyby kompilator ostrzegał nas, gdy próbujemy zrobić coś takiego!

Jedną z zalet parametrów szablonu w porównaniu z parametrami funkcji jest to, że parametry szablonu są stałymi czasowymi kompilacji. Oznacza to, że możemy skorzystać z możliwości wymagających wyrażeń stałych.

Więc jednym z rozwiązań jest użycie std::get() (które sprawdza granice w czasie kompilacji) zamiast operator[] (które nie sprawdza granic):

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void printElement3(const std::array<T, N>& arr)
{
    std::cout << std::get<3>(arr) << '\n'; // checks that index 3 is valid at compile-time
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr); // okay

    std::array arr2{ 9, 7 };
    printElement3(arr2); // compile error

    return 0;
}

Kiedy kompilator osiągnie wywołanie printElement3(arr2), utworzy instancję funkcji printElement3(const std::array<int, 2>&). Wewnątrz treści tej funkcji znajduje się linia std::get<3>(arr). Ponieważ długość parametru tablicy wynosi 2, jest to nieprawidłowy dostęp i kompilator wygeneruje błąd.

Alternatywnym rozwiązaniem jest użycie static_assert do samodzielnego sprawdzenia warunku wstępnego dotyczącego długości tablicy:

Powiązana treść

Warunki wstępne omówimy w lekcji 9.6 — Assert i static_assert.

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void printElement3(const std::array<T, N>& arr)
{
    // precondition: array length must be greater than 3 so element 3 exists
    static_assert (N > 3);

    // we can assume the array length is greater than 3 beyond this point

    std::cout << arr[3] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr); // okay

    std::array arr2{ 9, 7 };
    printElement3(arr2); // compile error

    return 0;
}

Kiedy kompilator osiągnie wywołanie printElement3(arr2), utworzy instancję funkcji printElement3(const std::array<int, 2>&). Wewnątrz treści tej funkcji znajduje się linia static_assert (N > 3). Ponieważ parametr N nietypowy szablonu ma wartość 2, I 2 > 3 jest false, kompilator wygeneruje błąd.

Kluczowa informacja

W powyższym przykładzie możesz się zastanawiać, dlaczego używamy static_assert (N > 3); zamiast static_assert (std::size(arr) > 3). Ten ostatni nie zostanie skompilowany przed C++23 ze względu na wadę językową wspomnianą w poprzedniej lekcji (17.2 — std::array długość i indeksowanie).

Zwrócenie std::array

Pomijając składnię, przekazanie std::array do funkcji jest koncepcyjnie proste — przekaż ją przez referencję (const). Ale co, jeśli mamy funkcję, która musi zwrócić std::array? Sprawa jest trochę bardziej skomplikowana. W przeciwieństwie do std::vector, std::array nie można jej przenosić, więc powrót a std::array według wartości spowoduje utworzenie kopii tablicy Elementy wewnątrz tablicy zostaną przesunięte, jeśli można je przenieść, lub skopiowane w przeciwnym razie.

Są tu dwie konwencjonalne opcje, a którą z nich należy wybrać, zależy od okoliczności.

Zwrócenie std::array według wartości

Zwrócenie std:array by wartości jest w porządku, jeśli wszystkie poniższe warunki są spełnione:

  • Tablica nie jest ogromny.
  • Typ elementu jest tani w kopiowaniu (lub przenoszeniu).
  • Kod nie jest używany w kontekście wrażliwym na wydajność.

W takich przypadkach zostanie utworzona kopia std::array , ale jeśli wszystkie powyższe są spełnione, spadek wydajności będzie niewielki i trzymanie się najbardziej konwencjonalnego sposobu zwracania danych do osoby wywołującej może być najlepsze wyboru.

#include <array>
#include <iostream>
#include <limits>

// return by value
template <typename T, std::size_t N>
std::array<T, N> inputArray() // return by value
{
	std::array<T, N> arr{};
	std::size_t index { 0 };
	while (index < N)
	{
		std::cout << "Enter value #" << index << ": ";
		std::cin >> arr[index];

		if (!std::cin) // handle bad input
		{
			std::cin.clear();
			std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
			continue;
		}
		++index;
	}

	return arr;
}

int main()
{
	std::array<int, 5> arr { inputArray<int, 5>() };

	std::cout << "The value of element 2 is " << arr[2] << '\n';

	return 0;
}

Ta metoda ma kilka zalet:

  • Wykorzystuje najbardziej konwencjonalny sposób zwracania danych do osoby wywołującej.
  • Jest oczywiste, że funkcja zwraca wartość.
  • Możemy zdefiniować tablicę i użyć funkcji do jej zainicjowania w pojedynczej instrukcji.

Jest też kilka wad:

  • Funkcja zwraca kopię tablicy i wszystkich jej elementów, co nie jest tanie.
  • Gdy wywołujemy funkcję, musimy jawnie podać argumenty szablonu, ponieważ nie ma parametru, z którego można by je wydedukować.

Zwrócenie std::array za pomocą parametru out

W przypadkach, gdy zwrot przez wartość jest zbyt kosztowny, możemy zamiast tego użyć parametru zewnętrznego, w tym przypadku jest to obiekt wywołujący odpowiedzialny za przekazywanie std::array przez niestałe odwołanie (lub przez adres), a funkcja może następnie modyfikować tę tablicę.

#include <array>
#include <limits>
#include <iostream>

template <typename T, std::size_t N>
void inputArray(std::array<T, N>& arr) // pass by non-const reference
{
	std::size_t index { 0 };
	while (index < N)
	{
		std::cout << "Enter value #" << index << ": ";
		std::cin >> arr[index];

		if (!std::cin) // handle bad input
		{
			std::cin.clear();
			std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
			continue;
		}
		++index;
	}

}

int main()
{
	std::array<int, 5> arr {};
	inputArray(arr);

	std::cout << "The value of element 2 is " << arr[2] << '\n';

	return 0;
}

Główną zaletą tej metody jest to, że nigdy nie jest tworzona żadna kopia tablicy, więc jest to wydajne.

Jest też kilka wad:

  • Ta metoda zwracania danych jest niekonwencjonalna i nie jest łatwo stwierdzić, czy funkcja modyfikuje argument.
  • Możemy użyć tej metody tylko do przypisania wartości do tablicy, a nie do jej inicjowania.
  • Takiej funkcji nie można używać do tworzenia obiektów tymczasowych.

Zwrócenie std::vector Zamiast tego

std::vector można przenosić i można ją zwrócić według wartości bez tworzenia kosztownych kopii. Jeśli zwracasz std::array według wartości, prawdopodobnie std::array nie jest constexpr i powinieneś rozważyć użycie (i zwrócenie) std::vector .

Czas quizu

Pytanie nr 1

Wykonaj następujący program:

#include <array>
#include <iostream>

int main()
{
    constexpr std::array arr1 { 1, 4, 9, 16 };
    printArray(arr1);

    constexpr std::array arr2 { 'h', 'e', 'l', 'l', 'o' };
    printArray(arr2);
    
    return 0;
}

Po uruchomieniu powinno wyświetlić:

The array (1, 4, 9, 16) has length 4
The array (h, e, l, l, o) has length 5

Pokaż rozwiązanie

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