如何编写一个函数,使其能够接受数组或向量作为输入?

8

我希望编写一个只有一个参数的C++函数,可以传入以下任一类型:

std::vector<int>
std::array<int>
int array[numElements]
int *ptr = new int[numElements]
etc

模板化是否是实现这个目标的最佳方法?

HolyBlackCat的建议是在不深入探究的情况下最好的选择... - L.C.
9
STL类似的方式将是使用接受起始和终止迭代器的模板化函数。 - Aconcagua
@Iamanon 这在很大程度上取决于您想如何在函数内部处理这些类型,以及您计划使用哪些操作。您能否给出一个伪代码示例来说明您的用例? - πάντα ῥεῖ
至少在std::span标准化之前,老式的方法也能正常工作。 - user657267
3
“etc”是“等等”的意思。你能保证只会使用std::vectorstd::arrayint *吗?那std::deque或任何整数容器呢?建议使用迭代器,这样可以指定满足算法要求的任何序列,如果需求改变或出现新的容器类型,这将避免限制自己。 - PaulMcKenzie
5个回答

14
如果你期望只能执行func(v),那是不可能的,因为我想不到任何方法使你的函数可以推断出动态分配的int[numElements]的大小。 一种好的方法是使用一对前置迭代器,也就是说,如果你只需要逐一迭代项,因为在某些容器上(如std::list)随机访问非常糟糕。
template<class FWIt>
void func(FWIt a, const FWIt b)
{
    while (a != b)
    {
        std::cout << "Value: " << *a << '\n';
        ++a;
    }
}

template<class T>
void func(const T& container)
{
    using std::begin;
    using std::end;
    func(begin(container), end(container));
}

以下内容可以正常运行:

int array[5] = {1, 2, 3, 4, 5};
func(array);

int* dynarray = new int[5]{1, 2, 3, 4, 5};
func(dynarray, dynarray + 5);

std::vector<int> vec{1, 2, 3, 4, 5};
func(vec);
func(vec.begin(), vec.end());

std::list<int> list{1, 2, 3, 4, 5};
func(list);

编辑:由于@DanielH的更改,现在也可以直接传递原始数组而不是两个指针,但仍无法使用动态分配的数组。


3
在您的前向迭代器“func”中,如果您正在处理指针类型(如迭代器),则无需使用“for_each”或“auto v”,而是可以直接递增第一个指针并对其进行解引用。例如:template<class FWIt> void func(const FWIt a, const FWIt b) { while (a != b) { std::cout << *a; ++a; } },但需要注意的是,这仅假设您仅使用指针类型。 - txtechhelp
1
更好的实现单参数 func 的方法是 using std::begin; using std::end; func(begin(container), end(container));。这样,如果 beginend 是容器命名空间中的自由函数,则可以正常工作,并且 std::beginstd::end 可以用于原始数组(可以通过引用接受)和调用 .begin.end 的类型。 - Daniel H
1
@Asu 对于 std::list 它将完美地工作。所有迭代器都可以被递增。你的示例函数甚至可以使用标准的 for 循环来处理 InputIterator,而不仅仅是 ForwardIterator。事实上,std::for_each 可以使用 for 循环定义,使用 template<class InputIt, class UnaryFunction> constexpr UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f) { for (; first != last; ++first) { f(*first); } return f; } - Daniel H
哦,好的 - 我误解了@txtechhelp的片段。我会进行编辑,确实没必要那么复杂。 - asu
1
“would work with the following”:远非完整:有序/无序集合,字符串,(不)有序映射(如果处理std::pair作为数据类型),双端队列等等[...]和许多其他的。 - Aconcagua
显示剩余2条评论

7

span 似乎是你正在寻找的。要么等待 C++20 :-) 或使用 GSL 中的 span。请参见什么是“span”,何时应该使用它? 下面是一个示例。

你没有说明想要一个接受可写或只读版本的输入数据的参数,因此示例同时展示了两者。

#include <array>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <vector>

#if defined(__has_include) && __has_include(<span>)
#include <span>
#endif

#ifdef __cpp_lib_span
using std::span;
#else
#include <gsl/gsl>
using gsl::span;
#endif

void funcDoSomething(span<int> data) {
    int value{41};
    for (auto &i: data) {
        i += value;
        ++value;
    }
}

void funcReadOnly(span<int const> data) {
    for (auto &i: data) {
        // ++i; won't compile since we have read-only span values
        std::cout << i << ' ';
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> stdvec{10, 11, 12};
    funcDoSomething(stdvec);
    funcReadOnly(stdvec);

    std::array<int, 3> stdarr{20, 21, 22};
    funcDoSomething(stdarr);
    funcReadOnly(stdarr);

    int carr[3]{30, 31, 32};
    funcDoSomething(carr);
    funcReadOnly(carr);

    auto ptr = std::unique_ptr<int[]>(new int[3]);
    ptr[0] = 40;
    ptr[1] = 41;
    ptr[2] = 42;
    funcDoSomething({ptr.get(), 3});
    funcReadOnly({ptr.get(), 3});

    return EXIT_SUCCESS;
}

4

使用模板化技术是实现这一目标的最佳方式吗?

这要看情况。如果您正在编写一个将用于进一步编译的头文件中的函数,那么最好使用模板化技术:

template <typename IntContainer>
void f(Container& c);

或者
template <typename IntContainer>
void f(const Container& c);

然而,如果实现只被编译一次,您应该考虑:
void f(gsl::span<int> sp);

或者

void f(gsl::span<const int> sp);

该函数使用了一个span。如果您还不知道它是什么,请阅读:

什么是“span”,何时应该使用它?

这个函数几乎可以直接使用你的所有变量:标准库容器std::vectorstd::array和定长数组可以直接传递,只有指针需要像这样调用:f(gsl::make_span{ptr, numElements})

PS-标准库中非常常见的第三种选择是将迭代器而不是容器作为参数。这也需要进行模板化,因此与第一种选项类似。


3
你不能将所有列出的类型放到一个单一的函数模板中。但是,你可以有函数模板重载,这将解决 std::vector<>, std::array<>Type array[numElements] 的问题。
template<typename Iter>
void funArray(const Iter begin, const Iter end) 
{
    std::cout << "Actual code here\n";
}

template<typename Container> void funArray(const Container& arr)
{
    funArray(std::begin(arr), std::end(arr)); //std::vector or std::array
}

现在你可以写:
int main()
{
    const std::size_t numElements = 5;
    std::vector<int> vec;
    std::array<int, numElements> arr;
    int array[numElements];
    int *ptr = new int[numElements];

    funArray(vec);
    funArray(arr);
    funArray(array);
    funArray(ptr, ptr+numElements); 
    return 0;
}

然而,对于动态分配的数组,您需要像用户@Asu建议的那样使用。
编辑:删除了多余的重载。

template<typename T, std::size_t N> void funArray(const T (&arr)[N]) {}

正如@Daniel H所指出的那样,上述用于C类型数组的函数模板重载是徒劳的,因为它可以通过第二个重载(template<typename Container>)直接推导到C类型数组。


1
你不需要使用 funArray 的第三个重载;如果边界已知,编译器将在第二个重载中推断出 T 适当的数组类型。此外,如果你使用 using std::begin; using std::end,然后调用 begin()end() 而不指定命名空间,如果 begin()end() 是容器命名空间中的自由函数,则会起作用;我知道这是 swap 的标准建议,我认为类似函数也是如此,但我不太确定。 - Daniel H
1
如果您没有第三个重载,但仍调用funArray(array),它将将Container推断为int [5]而不是int *,并等同于第三个重载。由于std :: end不能在指针上工作,因此您可以通过注释第三个重载并查看main仍然可以编译来检查它。 - Daniel H
@DanielH 谢谢你指出这个问题。我已经进行了更新。 - JeJo

0

如果它们都使用int,那么你可以简单地接受开始和结束指针。你可以在它们上面使用标准的算法,因为指针就是迭代器

void my_func(int const* begin, int const* end)
{
    std::for_each(begin, end, [](int i){
        std::cout << i << '\n';
    });
}

然后:

std::vector<int> v;
std::array<int> a;
int array[numElements];
int* ptr = new int[numElements];


my_func(v.data(), v.data() + v.size());

my_func(a.data(), a.data() + a.size());

my_func(std::begin(array), std::end(array));

my_func(ptr, ptr + numElements);

通过传递两个迭代器(泛型或其他类型),一个好处是你不必将整个容器传递给算法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接