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

