17-3 传递和返回 std::array
std::array 类型的对象可以像其他对象一样传递给函数。这意味着如果按值传递 std::array,将产生昂贵的复制操作。因此,我们通常通过 (const) 引用传递 std::array 以避免此类复制。
对于 std::array,元素类型和数组长度都是对象类型信息的一部分。因此,当我们使用 std::array 作为函数参数时,必须显式指定元素类型和数组长度:
#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(目前)不支持函数参数,因此我们无法在此处直接指定std::array并让编译器推断模板参数。
使用函数模板传递不同元素类型或长度的 std::array
要编写一个能接受任意元素类型或长度的 std::array 的函数,我们可以创建一个函数模板,将 std::array 的元素类型和长度都作为参数进行参数化,然后 C++ 会使用该函数模板实例化具有实际类型和长度的具体函数。
相关内容:
我们在第11.6课——函数模板中讲解了函数模板。
由于std::array的定义如下:
template<typename T, std::size_t N> // N is a non-type template parameter
struct array;
我们可以创建一个使用相同模板参数声明的函数模板:
#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;
}

在上例中,我们创建了一个名为 passByRef() 的单一函数模板,其参数类型为 std::array<T, N>。T 和 N 在前一行模板参数声明中定义:template <typename T, std::size_t N>。T 是标准类型模板参数,允许调用方指定元素类型。N 是类型为 std::size_t 的非类型模板参数,用于指定数组长度。
警告:
请注意 std::array 的非类型模板参数类型必须是 std::size_t,而非 int!这是因为 std::array 定义为 template<class T, std::size_t N> struct array;。若将 int 用作非类型模板参数的类型,编译器将无法将类型为 std::array<T, std::size_t> 的参数与类型为 std::array<T, int> 的参数匹配(且模板不会进行类型转换)。
因此当主函数调用 passByRef(arr) 时(其中 arr 定义为 std::array<int, 5>),编译器会实例化并调用 void passByRef(const std::array<int, 5>& arr)。arr2 和 arr3 的处理过程类似。
至此,我们已创建出单一函数模板,可实例化处理任意元素类型和长度的 std::array 参数!
若需定制,也可仅对两个模板参数中的一个进行模板化。如下例所示,我们仅对 std::array 的长度进行参数化,而元素类型则显式定义为 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;
}

自动非类型模板参数(C++20)
在声明函数模板的模板参数时,必须记住(或查阅)非类型模板参数的类型才能使用它,这相当麻烦。
在 C++20 中,我们可以在模板参数声明中使用 auto 关键字,让非类型模板参数从参数推导其类型:
#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;
}

如果您的编译器支持 C++20,则可以使用此代码。
静态断言数组长度
考虑以下模板函数,它与上面展示的函数类似:
#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;
}

虽然printElement3()在此情况下运行正常,但该程序中潜藏着一个可能让粗心程序员中招的漏洞。发现了?
上述程序会输出数组中索引为3的元素值。只要数组确实存在索引为3的有效元素,这段代码就没问题。然而编译器会毫无异议地接受索引3超出范围的数组参数。例如:
#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;
}

这将导致未定义行为。理想情况下,当我们尝试执行此类操作时,编译器应发出警告!
模板参数相较于函数参数的优势在于:模板参数是编译时常量。这意味着我们可以利用需要常量表达式的功能特性。
因此解决方案之一是使用std::get()(执行编译时边界检查)替代operator[](不进行边界检查):
#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;
}

当编译器遇到对 printElement3(arr2) 的调用时,它将实例化函数 printElement3(const std::array<int, 2>&)。该函数体内包含一行代码 std::get<3>(arr)。由于数组参数的长度为 2,这属于无效访问,编译器将报错。
替代方案是使用 static_assert 自行验证数组长度的先决条件:
相关知识:
我们在第 9.6 课——Assert 和 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;
}

当编译器执行到 printElement3(arr2) 的调用时,它将实例化函数 printElement3(const std::array<int, 2>&)。该函数体内包含静态断言语句 static_assert (N > 3)。由于模板非类型参数 N 的值为 2,且 2 > 3 为假,编译器将报错。
关键要点:
在上例中,您可能疑惑为何使用 static_assert(N > 3) 而非 static_assert(std::size(arr) > 3)。后者在 C++23 之前无法编译,原因在于前一课(17.2 -- std::array 长度与索引)提及的语言缺陷。
返回 std::array
抛开语法不谈,将 std::array 传递给函数在概念上很简单——通过(const)引用传递即可。但如果我们需要一个函数返回 std::array 呢?情况就复杂些了。与std::vector不同,std::array不支持移动操作,因此按值返回会生成数组副本。数组元素若支持移动则被移动,否则则被复制。
这里有两种常规方案,具体选择取决于具体情况。
按值返回 std::array
当以下所有条件都成立时,按值返回 std::array 是可行的:
- 数组规模不大。
- 元素类型复制(或移动)成本低廉。
- 代码未在性能敏感场景中使用。
此时虽会生成 std::array 的副本,但若满足上述所有条件,性能开销微乎其微。坚持采用最常规的数据返回方式可能是最佳选择。
#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;
}

这种方法有几个优点:
- 它采用最常规的方式将数据返回给调用方。
- 函数返回值的意图非常明确。
- 可通过单条语句定义数组并使用函数进行初始化。
但同时也存在若干缺点:
- 函数返回数组及其所有元素的副本,开销较大。
- 调用时必须显式提供模板参数,因为无法通过参数推断。
通过输出参数返回 std::array
当按值返回的开销过大时,我们可以改用输出参数。此时调用方需通过非 const 引用(或地址)传递 std::array,函数即可修改该数组。
#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;
}

该方法的主要优势在于从未创建数组副本,因此效率较高。
但也存在若干缺点:
- 这种返回数据的方式非传统,难以察觉函数正在修改参数。
- 此方法仅能用于向数组赋值,无法用于初始化数组。
- 此类函数无法用于生成临时对象。
测验时间
问题 #1
完成以下程序:
#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;
}
运行时,它应输出:

显示答案
#include <array>
#include <iostream>
template <typename T, std::size_t N>
void printArray(const std::array<T, N>& arr)
{
std::cout << "The array (";
auto separator {""};
for (const auto& e: arr)
{
std::cout << separator << e;
separator = ", ";
}
std::cout << ") has length " << N << '\n';
}
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;
}

浙公网安备 33010602011771号