Passing a std::array of unknown size to a function

C++C++11Stdarray

C++ Problem Overview


In C++11, 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?

C++ Solutions


Solution 1 - C++

> 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.

Solution 2 - C++

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

Solution 3 - C++

EDIT

C++20 tentatively includes std::span

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

Original Answer

What you want is 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);

Pretty cool, huh?

Solution 4 - C++

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

Solution 5 - C++

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

Solution 6 - C++

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?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAdrianView Question on Stackoverflow
Solution 1 - C++Andy ProwlView Answer on Stackoverflow
Solution 2 - C++Mark BView Answer on Stackoverflow
Solution 3 - C++sunchoView Answer on Stackoverflow
Solution 4 - C++musk'sView Answer on Stackoverflow
Solution 5 - C++David M. HelmuthView Answer on Stackoverflow
Solution 6 - C++Yakk - Adam NevraumontView Answer on Stackoverflow