157

How would I go about writing a function (or method) that takes a std::array of known type but unknown size?

// made up example
void mulArray(std::array<int, ?>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

During my search I only found suggestions to use templates, but those seems messy (method definitions in header) and excessive for what I'm trying to accomplish.

Is there a simple way to make this work, as one would with plain C-style arrays?

6
  • 2
    Arrays have no bounds checking or know what size they are. Therefore, you must wrap them in something or consider use of std::vector. Commented Jun 17, 2013 at 20:33
  • 22
    If templates seem messy and excessive to you, you should get over that feeling. They are commonplace in C++. Commented Jun 17, 2013 at 20:35
  • 2
    Understood. If this a limitation of their nature, I'll have to accept that. The reason I thought about avoiding std::vector (which works great for me), is that it's allocated on the heap. Since these arrays will be tiny and looped through in every iteration of the program, I thought a std::array might perform a bit better. I think I will use a C-style array then, my program is not complex. Commented Jun 17, 2013 at 20:38
  • 21
    @Adrian Your way of thinking about performance is entirely wrong. Don't try to make micro optimizations before you even have a functional program. And after you do have a program, don't guess on what should be optimized, instead let a profiler tell you what part of the program should be optimized. Commented Jun 17, 2013 at 20:46
  • 2
    C++20 adds std::span which understands std::array. This signature works: void mulArray(std::span<int>& arr, const int multiplier). Commented Feb 3, 2024 at 18:07

6 Answers 6

132

Is there a simple way to make this work, as one would with plain C-style arrays?

No. You really cannot do that unless you make your function a function template (or use another sort of container, like an std::vector, as suggested in the comments to the question):

template<std::size_t SIZE>
void mulArray(std::array<int, SIZE>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

Here is a live example.

Sign up to request clarification or add additional context in comments.

12 Comments

The OP asks if there is any another solution but templates.
@Adrian: Unfortunately there is no other solution, if you want your function to work generically on arrays of any size...
Correct: there is no other way. Since each std::array with a different size is a different type, you need to write a function that can work on different types. Hence, templates are the solution for std::array.
The beautiful part about using a template here is that you can make this even more generic, so that it works with any sequence container, as well as standard arrays: template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
@BenjaminLindley: Of course, that assumes that he can put the code in the header at all.
|
38

The size of the array is part of the type, so you can't do quite what you want. There are a couple alternatives.

Preferred would be to take a pair of iterators:

template <typename Iter>
void mulArray(Iter first, Iter last, const int multiplier) {
    for(; first != last; ++first) {
        *first *= multiplier;
    }
}

Alternately, use vector instead of array, which allows you to store the size at runtime rather than as part of its type:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

2 Comments

I think this is the superior solution; if you are going to go through the trouble of making a template, make it totally generic with iterators which will let you use any container (array,list, vector,even old school C pointers, etc) with no downside. Thanks for the hint.
This results in bloated binaries because new code for mulArray is generated for each call with a unique array argument. Unfortunately, this is all that is available to OP's C++ requirement. These days, C++20's std::span is a better solution.
25

In C++20, std::span solves the problem:

void mulArray(std::span<int> arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

https://en.cppreference.com/w/cpp/container/span

Before C++20, you can use something like gsl::span, which is available in the Guideline Support Library described in the C++ Core Guidelines:

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#SS-views

You can find an open-source header-only implementation of the GSL here:

https://github.com/Microsoft/GSL

With gsl::span, you can do this:

// made up example
void mulArray(gsl::span<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

The problem with std::array is that its size is part of its type, so you'd have to use a template in order to implement a function that takes an std::array of arbitrary size.

gsl::span on the other hand stores its size as run-time information. This allows you to use one non-template function to accept an array of arbitrary size. It will also accept other contiguous containers:

std::vector<int> vec = {1, 2, 3, 4};
int carr[] = {5, 6, 7, 8};

mulArray(vec, 6);
mulArray(carr, 7);

Comments

8

I tried below and it just worked for me.

#include <iostream>
#include <array>

using namespace std;

// made up example
void mulArray(auto &arr, const int multiplier) 
{
    for(auto& e : arr) 
    {
        e *= multiplier;
    }
}

void dispArray(auto &arr)
{
    for(auto& e : arr) 
    {
        std::cout << e << " ";
    }
    std::cout << endl;
}

int main()
{

    // lets imagine these being full of numbers
    std::array<int, 7> arr1 = {1, 2, 3, 4, 5, 6, 7};
    std::array<int, 6> arr2 = {2, 4, 6, 8, 10, 12};
    std::array<int, 9> arr3 = {1, 1, 1, 1, 1, 1, 1, 1, 1};

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);
    
    mulArray(arr1, 3);
    mulArray(arr2, 5);
    mulArray(arr3, 2);

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);

    return 0;
}

Output:

1 2 3 4 5 6 7 

2 4 6 8 10 12 

1 1 1 1 1 1 1 1 1

3 6 9 12 15 18 21 

10 20 30 40 50 60 

2 2 2 2 2 2 2 2 2

5 Comments

This is not valid C++, but rather an extension. These functions are templates, even without template.
I've looked into this and it appears that auto foo(auto bar) { return bar * 2; } is not currently valid C++ even though it compiles in GCC7 with the C++17 flag set. From reading here, function parameters declared as auto are part of the Concepts TS which should eventually be part of C++20.
Warning C26485
Unfortunately, with an "auto" parameter, the compiler generates a new function each time the argument changes. Try pasting this into [compiler explorer](godbolt.org) and you will see that every call results in a new function being generated unless you always pass the same argument (ex.: always "arr1").
@pixelgrease: That is incorrect. It's just an abbreviated function template. That means each instantiation has only one address. Optimizers can of course inline them.
7

Absolutely, there is a simple way in C++11 to write a function that takes a std::array of known type, but unknown size.

If we are unable to pass the array size to the function, then instead, we can pass the memory address of where the array starts along with a 2nd address of where the array ends. Later, inside of the function, we can use these 2 memory addresses to calculate the size of the array!

#include <iostream>
#include <array>

// The function that can take a std::array of any size!
void mulArray(int* piStart, int* piLast, int multiplier){

     // Calculate the size of the array (how many values it holds)
     unsigned int uiArraySize = piLast - piStart;

     // print each value held in the array
     for (unsigned int uiCount = 0; uiCount < uiArraySize; uiCount++)     
          std::cout << *(piStart + uiCount) * multiplier << std::endl;
}

int main(){   

     // initialize an array that can can hold 5 values
     std::array<int, 5> iValues{ 5, 10, 1, 2, 4 };

     // Provide a pointer to both the beginning and end addresses of 
     // the array.
     mulArray(iValues.begin(), iValues.end(), 2);

     return 0;
}

Output at Console:

10, 20, 2, 4, 8

Comments

2

This can be done, but it takes a few steps to do cleanly. First, write a template class that represents a range of contiguous values. Then forward a template version that knows how big the array is to the Impl version that takes this contiguous range.

Finally, implement the contig_range version. Note that for( int& x: range ) works for contig_range, because I implemented begin() and end() and pointers are iterators.

template<typename T>
struct contig_range {
  T* _begin, _end;
  contig_range( T* b, T* e ):_begin(b), _end(e) {}
  T const* begin() const { return _begin; }
  T const* end() const { return _end; }
  T* begin() { return _begin; }
  T* end() { return _end; }
  contig_range( contig_range const& ) = default;
  contig_range( contig_range && ) = default;
  contig_range():_begin(nullptr), _end(nullptr) {}

  // maybe block `operator=`?  contig_range follows reference semantics
  // and there really isn't a run time safe `operator=` for reference semantics on
  // a range when the RHS is of unknown width...
  // I guess I could make it follow pointer semantics and rebase?  Dunno
  // this being tricky, I am tempted to =delete operator=

  template<typename T, std::size_t N>
  contig_range( std::array<T, N>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, std::size_t N>
  contig_range( T(&arr)[N] ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, typename A>
  contig_range( std::vector<T, A>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
};

void mulArrayImpl( contig_range<int> arr, const int multiplier );

template<std::size_t N>
void mulArray( std::array<int, N>& arr, const int multiplier ) {
  mulArrayImpl( contig_range<int>(arr), multiplier );
}

(not tested, but design should work).

Then, in your .cpp file:

void mulArrayImpl(contig_range<int> rng, const int multiplier) {
  for(auto& e : rng) {
    e *= multiplier;
  }
}

This has the downside that the code that loops over the contents of the array doesn't know (at compile time) how big the array is, which could cost optimization. It has the advantage that the implementation doesn't have to be in the header.

Be careful about explicitly constructing a contig_range, as if you pass it a set it will assume that the set data is contiguous, which is false, and do undefined behavior all over the place. The only two std containers that this is guaranteed to work on are vector and array (and C-style arrays, as it happens!). deque despite being random access isn't contiguous (dangerously, it is contiguous in small chunks!), list is not even close, and the associative (ordered and unordered) containers are equally non-contiguous.

So the three constructors I implemented where std::array, std::vector and C-style arrays, which basically covers the bases.

Implementing [] is easy as well, and between for() and [] that is most of what you want an array for, isn't it?

4 Comments

Isn't this just offsetting the template to somewhere else?
@GManNickG sort of. The header gets a really short template function with next to no implementation details. The Impl function is not a template function, and so you can happily hide the implementation in the .cpp file of your choice. It is a really crude kind of type erasure, where I extract the ability to iterate over contiguous containers into a simpler class, and then pass that through... (while multArrayImpl takes a template as an argument, it isn't a template itself).
I understand that this array view/array proxy class is sometimes useful. My suggestion would be to pass the begin/end of the container in the constructor so you wouldn't have to write a constructor for each container. Also I wouldn't write '&*std::begin(arr)' as dereferencing and taking the address is unnecessary here as std::begin/std::end already return an iterator.
@Ricky65 If you use iterators, you need to expose the implementation. If you use pointers, you do not. The &* dereferences the iterator (which may not be a pointer), then makes a pointer to the address. For contiguous memory data, the pointer to begin and the pointer to one-past-the end are also random access iterators, and they are the same type for every contiguous range over a type T.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.