8

I would like to write a C++ function with one argument such that one can pass in either any of the following types:

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

Would templating be the best way to accomplish this?

JeJo
  • 30,635
  • 6
  • 49
  • 88
24n8
  • 1,898
  • 1
  • 12
  • 25
  • HolyBlackCat suggestion is the best you can do without going too deep down the rabbit hole... – L.C. Nov 11 '18 at 19:32
  • 9
    The STL-like way would be templated function accepting begin and end iterators. – Aconcagua Nov 11 '18 at 19:37
  • _@Iamanon_ That heavily depends on how you want to deal with these types inside your function, and which operations you're planning to use. Can you give a _pseudo code_ example for your use case please? – πάντα ῥεῖ Nov 11 '18 at 19:37
  • At least until `std::span` is standardized the old-fashioned way works fine. – user657267 Nov 11 '18 at 19:38
  • 3
    `etc`. What is meant by "etc"? Can you guarantee it will stay only `std::vector`, `std::array`, or `int *`? How about `std::deque`, or any container of integers? Passing iterators, as suggested, allows you to specify any sequence that satisfies your algorithm. This keeps you from painting yourself into a corner if requirements change, or a new container type comes along, etc. – PaulMcKenzie Nov 11 '18 at 19:46

5 Answers5

14

If you expect to just be able to do func(v) you cannot, because there's no way I can think of that your function could deduce the size of the dynamically allocated int[numElements].

A good way you could wrap this is to take a pair of forward iterators, that is, if you only need iterating over items one by one, since random access is very bad on some containers like 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));
}

This would work with the following:

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

Edit: This also works by passing raw arrays directly rather than as two pointers thanks to @DanielH's change (but still won't work with dynamically allocated arrays).

asu
  • 1,875
  • 17
  • 27
  • 3
    Small note: in your forward iterator `func`, there's no need for `for_each` or `auto v`, if you're dealing with pointer types (like iterators); instead, you can just increment the first pointer and deference it, example: `template void func(const FWIt a, const FWIt b) { while (a != b) { std::cout << *a; ++a; } }` .. this does assume you're _only_ working with pointer types though .. – txtechhelp Nov 11 '18 at 19:58
  • This wouldn't work for `std::list` though and the question seems to be focused around supporting as much container types as possible - so assuming we are dealing with forward iterators seems good enough IMO. – asu Nov 11 '18 at 20:29
  • 1
    A better way to implement the one-argument `func` would be as `using std::begin; using std::end; func(begin(container), end(container));`. That way it works if `begin` and `end` are free functions in the container's namespace, and `std::begin` and `std::end` work for raw arrays (which can be accepted by reference) and for types where you call `.begin` and `.end`. – Daniel H Nov 11 '18 at 20:30
  • 1
    @Asu It will work perfectly fine for `std::list`. All iterators can be incremented. Your example function would even work for *InputIterator*s, not just *ForwardIterator*s, using a standard `for` loop. In fact, `std::for_each` [can be defined with a `for` loop](https://en.cppreference.com/w/cpp/algorithm/for_each#Possible_implementation) using `template constexpr UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f) { for (; first != last; ++first) { f(*first); } return f; }` – Daniel H Nov 11 '18 at 20:31
  • Oh okay - I've misunderstood @txtechhelp's snippet. Will edit accordingly, it's indeed less overkill. – asu Nov 11 '18 at 20:38
  • Also edited to apply your change to the one-argument `func`, thanks! – asu Nov 11 '18 at 20:42
  • 1
    *'would work with the following'"*: far from being complete: ordered/unordered sets, strings, (un-)ordered maps (if handling `std::pair` as data type), deques, ... (*'[...] and many more:'*). – Aconcagua Nov 12 '18 at 05:33
7

span seems to be what you are looking for. Either wait for C++20 :-) or use span from the GSL. See What is a “span” and when should I use one? . Example below.

You didn't say if you wanted an argument that take a writable or read-only version of the indata so the example shows both.

#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;
}
Bo R
  • 2,334
  • 1
  • 9
  • 17
4

Would templating be the best way to accomplish this?

It depends. If you're writing a function that goes in a header, and thus can be used for further compilation later, then - yes, probably:

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

or

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

However, if the implementation is compiled just once, you should consider:

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

or

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

which uses a span. If you haven't heard about them, read:

What is a "span" and when should I use one?

this function will be able to take almost all of your variables as-is: The std::vector, the std::array and the plain (sized) array can be passed with no extra syntax. For the pointer, though, you will need to call something like f(gsl::make_span{ptr, numElements}).

PS - A third option, very common in the standard library, is to take interators, rather than the container, as the parameter. This would also require templating so it's similar to the first option.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
3

You can not get all the listed types to one single function template. But, you could have function template overloads, which will solve the problem for std::vector<>, std::array<> and 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
}

Now you can write:

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

However, for the dynamically allocated array, you need to use as user @Asu suggested.


Edit: Removed the redundant overload.

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

As @Daniel H pointed out that, the above function template overload for C type arrays, are futile, since it can be handled by the second overload(template<typename Container>), by deducing directly to C type arrays.

JeJo
  • 30,635
  • 6
  • 49
  • 88
  • 1
    You don't need the third overload of `funArray`; the compiler will deduce `T` in the second overload to the appropriate array type if the bounds are known. Also if you use `using std::begin; using std::end` and then just call `begin()` and `end()` without specifying namespace, it'll work if `begin()` and `end()` are free functions in the container's namespace; I know that's the standard recommendation for `swap` and I think it is for similar functions but I'm not as sure. – Daniel H Nov 11 '18 at 20:44
  • 1
    If you don't have the third overload and still call `funArray(array)`, it'll deduce `Container` to `int[5]`, not `int*`, and be equivalent to the third overload. Since `std::end` can't work on just a pointer, you can check that by commenting out the third overload and seeing that `main` still compiles as-is. – Daniel H Nov 11 '18 at 21:57
  • @DanielH thanks for pointing that out. I have updated with that. – JeJo Nov 12 '18 at 10:56
0

If they all use int then you could simply accept beginning and end pointers. You can use the standard algorithms on them because pointers are iterators.

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

Then:

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

A benefit of passing two iterators (generic or otherwise) is that you do not have to pass the whole container to the algorithm.

Galik
  • 47,303
  • 4
  • 80
  • 117