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

image


传递不同元素类型的 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;
}

image

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

image

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

image

在上例中,我们创建了一个名为 passByRef() 的函数模板,其参数类型为 const std::vector&。T 在前一行模板参数声明中定义:template 。T 是标准类型模板参数,允许调用方指定元素类型。

因此当我们在 main() 中调用 passByRef(primes) 时(其中 primes 定义为 std::vector),编译器将实例化并调用 void passByRef(const std::vector& arr)。

当我们在 main() 中调用 passByRef(dbl) 时(其中 dbl 定义为 std::vector),编译器将实例化并调用 void passByRef(const std::vector& arr)。

由此,我们创建了一个单一函数模板,能够实例化处理任意元素类型和长度的 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;
}

image

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

image

这两种方法都能接受任何能通过编译的类型作为参数。当编写可能需要操作多种类型(不仅限于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;
}

image

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

image

这将导致未定义行为。

一种解决方案是在调用 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()), 报错如下:
image

image

显示答案

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