16-4 传递 std::vector
与其他对象一样,std::vector 类型的对象也可以传递给函数。这意味着如果按值传递 std::vector,将产生昂贵的复制操作。因此,我们通常通过 (const) 引用传递 std::vector 以避免此类复制。
对于 std::vector,元素类型是对象类型信息的一部分。因此,当将 std::vector 用作函数参数时,必须显式指定元素类型:
#include <iostream>
#include <vector>
void passByRef(const std::vector<int>& arr) // we must explicitly specify <int> here
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes);
return 0;
}

传递不同元素类型的 std::vector
由于我们的 passByRef() 函数期望接收 std::vector
#include <iostream>
#include <vector>
void passByRef(const std::vector<int>& arr)
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // ok: this is a std::vector<int>
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // compile error: std::vector<double> is not convertible to std::vector<int>
return 0;
}

在 C++17 或更新版本中,你可以尝试使用 CTAD 来解决这个问题:
#include <iostream>
#include <vector>
void passByRef(const std::vector& arr) // compile error: CTAD can't be used to infer function parameters
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 }; // okay: use CTAD to infer std::vector<int>
passByRef(primes);
return 0;
}

尽管CTAD会在定义向量时尝试从初始化表达式推断元素类型,但它(目前)不适用于函数参数。
我们之前遇到过类似问题:存在仅参数类型不同的重载函数。此时正是运用函数模板的绝佳场景!我们可以创建一个将元素类型作为参数的函数模板,C++将利用该模板根据实际类型实例化函数。
相关内容:
第11.6节 函数模板 中详细讲解了函数模板。
我们可以创建一个使用相同模板参数声明的函数模板:
#include <iostream>
#include <vector>
template <typename T>
void passByRef(const std::vector<T>& arr)
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // ok: compiler will instantiate passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // ok: compiler will instantiate passByRef(const std::vector<double>&)
return 0;
}

在上例中,我们创建了一个名为 passByRef() 的函数模板,其参数类型为 const std::vector
因此当我们在 main() 中调用 passByRef(primes) 时(其中 primes 定义为 std::vector
当我们在 main() 中调用 passByRef(dbl) 时(其中 dbl 定义为 std::vector
由此,我们创建了一个单一函数模板,能够实例化处理任意元素类型和长度的 std::vector 参数的函数!
使用泛型模板或简写函数模板传递 std::vector
我们还可以创建一个能接受任意类型对象的函数模板:
#include <iostream>
#include <vector>
template <typename T>
void passByRef(const T& arr) // will accept any type of object that has an overloaded operator[]
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // ok: compiler will instantiate passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // ok: compiler will instantiate passByRef(const std::vector<double>&)
return 0;
}

在 C++20 中,我们可以使用简写函数模板(通过 auto 参数)实现相同效果:
#include <iostream>
#include <vector>
void passByRef(const auto& arr) // abbreviated function template
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // ok: compiler will instantiate passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // ok: compiler will instantiate passByRef(const std::vector<double>&)
return 0;
}

这两种方法都能接受任何能通过编译的类型作为参数。当编写可能需要操作多种类型(不仅限于std::vector)的函数时,这种特性就显得尤为重要。例如,上述函数同样适用于std::array、std::string,甚至某些我们尚未考虑过的其他类型。
此方法的潜在弊端在于:若函数接收的对象类型虽能编译通过,但在语义上却毫无意义,则可能引发错误。
对数组长度进行断言
考虑以下模板函数,它与上面展示的函数类似:
#include <iostream>
#include <vector>
template <typename T>
void printElement3(const std::vector<T>& arr)
{
std::cout << arr[3] << '\n';
}
int main()
{
std::vector arr{ 9, 7, 5, 3, 1 };
printElement3(arr);
return 0;
}

虽然printElement3(arr)在此情况下运行正常,但该程序中潜藏着可能让粗心程序员中招的漏洞。发现了?
上述程序会输出数组中索引为3的元素值。只要数组存在有效的索引3元素,这没有问题。然而编译器会毫无异议地接受索引3超出范围的数组参数。例如:
#include <iostream>
#include <vector>
template <typename T>
void printElement3(const std::vector<T>& arr)
{
std::cout << arr[3] << '\n';
}
int main()
{
std::vector arr{ 9, 7 }; // a 2-element array (valid indexes 0 and 1)
printElement3(arr);
return 0;
}

这将导致未定义行为。
一种解决方案是在调用 arr.size() 时进行静态断言,这能在调试构建配置下捕获此类错误。由于 std::vector::size() 是非 constexpr 函数,我们只能在此处进行运行时断言。
提示:
更优方案是在需要对数组长度进行断言时避免使用 std::vector。使用支持常量表达式数组的类型(如 std::array)可能是更优选择,因为常量表达式数组的长度可通过 static_assert 进行检查。我们将在后续第 17.3 节 传递和返回 std::array 中详细讲解。
最佳方案是避免编写依赖用户传入最小长度向量的函数。
测验时间
问题 #1
编写一个函数,接受两个参数:一个 std::vector 和一个索引。如果索引超出范围,则打印错误信息。如果索引在范围内,则打印元素的值。
以下示例程序应能编译通过:
#include <iostream>
#include <vector>
// Write your printElement function here
int main()
{
std::vector v1 { 0, 1, 2, 3, 4 };
printElement(v1, 2);
printElement(v1, 5);
std::vector v2 { 1.1, 2.2, 3.3 };
printElement(v2, 0);
printElement(v2, -1);
return 0;
}
并产生以下结果:
The element has value 2
Invalid index
The element has value 1.1
Invalid index
谨记:这里我写错了,index < 0 || index > static_cast
(arr.size()), 报错如下:

显示答案
#include <iostream>
#include <vector>
// index needs to be an int, not a std::size_t, otherwise we won't be able to detect if the user passes in a negative index
template <typename T>
void printElement(const std::vector<T>& arr, int index)
{
if (index < 0 || index >= static_cast<int>(arr.size())) // In C++20, could use std::ssize(arr) to avoid the cast
std::cout << "Invalid index\n";
else
std::cout << "The element has value " << arr[static_cast<std::size_t>(index)] << '\n';
}
int main()
{
std::vector v1 { 0, 1, 2, 3, 4 };
printElement(v1, 2);
printElement(v1, 5);
std::vector v2 { 1.1, 2.2, 3.3 };
printElement(v2, 0);
printElement(v2, -1);
return 0;
}


浙公网安备 33010602011771号