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;
}

image

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;
}

image

在上例中,我们创建了一个名为 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;
}

image


自动非类型模板参数(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;
}

image

如果您的编译器支持 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;
}

image

虽然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;
}

image

这将导致未定义行为。理想情况下,当我们尝试执行此类操作时,编译器应发出警告!

模板参数相较于函数参数的优势在于:模板参数是编译时常量。这意味着我们可以利用需要常量表达式的功能特性。

因此解决方案之一是使用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;
}

image

当编译器遇到对 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;
}

image

当编译器执行到 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;
}

image

这种方法有几个优点:

  • 它采用最常规的方式将数据返回给调用方。
  • 函数返回值的意图非常明确。
  • 可通过单条语句定义数组并使用函数进行初始化。

但同时也存在若干缺点:

  • 函数返回数组及其所有元素的副本,开销较大。
  • 调用时必须显式提供模板参数,因为无法通过参数推断。

通过输出参数返回 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;
}

image

该方法的主要优势在于从未创建数组副本,因此效率较高。

但也存在若干缺点:

  • 这种返回数据的方式非传统,难以察觉函数正在修改参数。
  • 此方法仅能用于向数组赋值,无法用于初始化数组。
  • 此类函数无法用于生成临时对象。

测验时间

问题 #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;
}

运行时,它应输出:

image

显示答案

#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;
}
posted @ 2026-01-10 11:20  游翔  阅读(11)  评论(0)    收藏  举报