与任何其他对象一样,std::vector 类型的对象也可以作为实参传递给函数。这意味着,如果我们按值传递 std::vector,将会产生一次开销巨大的拷贝。因此,通常采用(const)引用方式传递 std::vector,以避免此类拷贝。
对于 std::vector,其元素类型属于对象类型信息的一部分。因此,当我们把 std::vector 用作函数形参时,必须显式指定元素类型:
#include <iostream>
#include <vector>
void passByRef(const std::vector<int>& arr) // 必须在此处显式指定 <int>
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes);
return 0;
}
传递不同元素类型的 std::vector
由于 passByRef() 函数期望的是 std::vector<int>
,我们无法向其传递元素类型不同的 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); // 正确:这是一个 std::vector<int>
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // 编译错误:std::vector<double> 无法转换为 std::vector<int>
return 0;
}
在 C++17 或更高版本中,你可能会尝试使用 CTAD(类模板实参推导)来解决这一问题:
#include <iostream>
#include <vector>
void passByRef(const std::vector& arr) // 编译错误:CTAD 无法用于推导函数形参
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 }; // 正确:使用 CTAD 推导出 std::vector<int>
passByRef(primes);
return 0;
}
尽管定义 vector 时可利用 CTAD 根据初始化列表推导出元素类型,但 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); // 正确:编译器将实例化 passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // 正确:编译器将实例化 passByRef(const std::vector<double>&)
return 0;
}
上例中,我们创建了名为 passByRef 的单一函数模板,其形参类型为 const std::vector<T>&
。模板参数声明 template <typename T>
中的 T
为标准类型模板形参,由调用者指定元素类型。
因此,当从 main() 调用 passByRef(primes)
(primes 为 std::vector<int>
)时,编译器将实例化并调用 void passByRef(const std::vector<int>& arr)
。
当从 main() 调用 passByRef(dbl)
(dbl 为 std::vector<double>
)时,编译器将实例化并调用 void passByRef(const std::vector<double>& arr)
。
于是,我们仅用一条函数模板即可实例化出支持任意元素类型和长度的 std::vector
处理函数!
使用泛型模板或缩写函数模板传递 std::vector
我们还可以编写接受任何类型对象的函数模板:
#include <iostream>
#include <vector>
template <typename T>
void passByRef(const T& arr) // 接受任何拥有 operator[] 重载的类型
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // 正确:实例化为 passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // 正确:实例化为 passByRef(const std::vector<double>&)
return 0;
}
在 C++20 中,可使用缩写函数模板(通过 auto 形参)完成同样功能:
#include <iostream>
#include <vector>
void passByRef(const auto& arr) // 缩写函数模板
{
std::cout << arr[0] << '\n';
}
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // 正确:实例化为 passByRef(const std::vector<int>&)
std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // 正确:实例化为 passByRef(const std::vector<double>&)
return 0;
}
这两种方式都会接受任何能够编译通过的类型实参。当我们希望函数不仅适用于 std::vector
,还可能适用于 std::array
、std::string
乃至尚未考虑到的类型时,这种方式尤为便利。例如,上述函数亦可与 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 }; // 仅有 2 个元素(合法下标为 0 和 1)
printElement3(arr);
return 0;
}
这将导致未定义行为。
一种做法是对 arr.size()
进行断言,在调试构建配置下捕获此类错误。由于 std::vector::size()
不是 constexpr 函数,我们只能进行运行时断言。
建议
更优的做法是:若需在编译期断言数组长度,应避免使用 std::vector
,而选用支持 constexpr 数组的类型(例如 std::array
),因为可以对 constexpr 数组长度进行 static_assert
。我们将在后续课程 17.3 —— 传递与返回 std::array 中进行讨论。
最佳方案则是:首先避免编写依赖用户传入最小长度 vector 的函数。
小测验
问题 1
编写一个函数,接收两个形参:一个 std::vector
与一个下标。若下标越界,打印错误信息;若下标合法,打印该元素的值。
以下示例程序应能编译:
#include <iostream>
#include <vector>
// 在此处编写 printElement 函数
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