传递 std::vector

与任何其他对象一样,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::arraystd::string 乃至尚未考虑到的类型时,这种方式尤为便利。例如,上述函数亦可与 std::arraystd::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

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"