Introduction
Say we have an array of existing data (could be a plain array or in a container like std::vector
). And say that there's a function which requires read-only access to a portion of our array. Put another way, the function needs to read values from an array which is a subset of our existing array. In this situation, we could make another smaller array which contains the required subset of elements from our existing array, and pass that into the function.
Example A
void Func(const vector<int> &arr)
{
for( const int value : arr )
cout << value << endl;
}
int main()
{
const vector<int> arr = { 1, 2, 3, 4, 5 };
const vector<int> subset(arr.data() + 1, arr.data() + 4);
Func(subset);
return 0;
}
But this is inefficient as we had to make a copy of existing data. To share the data, we could pass a pointer to the first required element, along with the number of elements to use.
Example B
void Func(const int *const pArr, const int n)
{
for( int i = 0; i < n; ++i )
cout << pArr[i] << endl;
}
int main()
{
const vector<int> arr = { 1, 2, 3, 4, 5 };
Func(
arr.data() + 1, 3 );
return 0;
}
Notice how Func
now has to work directly with a raw pointer which is not very safe. Another approach would be to pass two iterators which define the bounds of the required data. But we would still miss having helpful functions like size
and the various comparison operators. We also still lack the ability to easily store the subset array into a standard container with automatic element-wise comparisons.
A view over part of an array
What we need is a wrapper class which will behave like an array, but which will share data with an existing array. Such a wrapper would behave like a read-only 'view' of a portion of our existing array. Let's name the wrapper ArrayView
and see how it could be used:
Example C
void Func(const ArrayView<int> &arr)
{
for( const int value : arr )
cout << value << endl;
}
int main()
{
const vector<int> arr = { 1, 2, 3, 4, 5 };
Func(ArrayView<int>(
arr.data() + 1, 3 ));
return 0;
}
That's it! Func
gets to use the nice vector-like interface as in Example A, and we keep the efficiency of sharing existing data as in Example B.
Taking notes from the flyweight software design pattern, we can implement a lightweight and efficient ArrayView
class. Our ArrayView
class should aim to:
- Provide a more efficient alternative than making a copy of a portion of an array.
- Provide a safer alternative than passing part of an array around using a raw pointer.
- Provide a helpful interface to the user, similar to
std::vector
, to increase code readability. - Be readily storable in standard containers like
std::vector
or (as the key) in std::map
.
Because the implementation is really quite simple, without further ado, the ArrayView
class implementation is presented below.
#pragma once
#include <utility>
#include <algorithm>
#include <stdexcept>
template <class T>
class ArrayView
{
public:
typedef const T * Ptr;
private:
Ptr m_pData;
size_t m_length;
public:
ArrayView() :
m_pData(nullptr),
m_length(0)
{
}
ArrayView(const Ptr pData, const size_t length) :
m_pData(pData),
m_length(length)
{
}
Ptr begin() const { return m_pData; }
Ptr end() const { return m_pData + m_length; }
Ptr cbegin() const { return m_pData; }
Ptr cend() const { return m_pData + m_length; }
bool empty() const { return m_length == 0; }
size_t size() const { return m_length; }
T operator [] (const size_t pos) const { return m_pData[pos]; }
Ptr data() const { return m_pData; }
T at(const size_t pos) const
{
if( pos < m_length )
return m_pData[pos];
throw std::out_of_range("pos");
}
void swap(ArrayView &other) { std::swap(*this, other); }
void clear() { *this = ArrayView(); }
void pop_back() { if( !empty() ) --m_length; }
void pop_front() { if( !empty() ) { ++m_pData; --m_length; } }
bool operator < (const ArrayView &other) const { return compare(other) < 0; }
bool operator > (const ArrayView &other) const { return other < *this; }
bool operator <= (const ArrayView &other) const { return !(*this > other); }
bool operator >= (const ArrayView &other) const { return !(*this < other); }
bool operator == (const ArrayView &other) const { return compare(other) == 0; }
bool operator != (const ArrayView &other) const { return !(*this == other); }
int compare(const ArrayView &other) const
{
const size_t minLength = std::min(m_length, other.m_length);
for( size_t i = 0; i < minLength; ++i )
{
if( m_pData[i] < other.m_pData[i] )
return -1;
if( m_pData[i] > other.m_pData[i] )
return 1;
}
if( m_length < other.m_length )
return -1;
if( m_length > other.m_length )
return 1;
return 0;
}
};
And here's some code which uses it.
#include "ArrayView.h"
#include <cassert>
using namespace std;
int main()
{
const int data[] = { 1, 2, 3, 4, 5 };
ArrayView<int> arr1(data, 3); ArrayView<int> arr2(data + 2, 3);
assert(arr1 != arr2); assert(arr1 < arr2); assert(!arr1.empty()); assert(arr1.size() == 3);
assert(arr1[0] == 1); assert(arr1[1] == 2);
assert(arr1[2] == 3);
arr1.swap(arr2);
assert(arr1[0] == 3); assert(arr1[1] == 4);
assert(arr1[2] == 5);
return 0;
}
The code above is littered with comments to make enough sense. So now that we have ArrayView
, we can create all kinds of other interesting views from it.
A view over part of a string
Subclassed from ArrayView
, StringView
's implementation is straightforward. We really only need to provide a bunch of sensible constructors.
#pragma once
#include "ArrayView.h"
#include <string>
struct StringView : ArrayView<char>
{
StringView()
{
}
StringView(const char *const pStr, const size_t length) :
ArrayView(pStr, length)
{
}
StringView(const char *const pStr) :
ArrayView(pStr, strlen(pStr))
{
}
std::string str() const
{
return std::string(m_pData, m_length);
}
};
And here's some code which takes it for a spin.
#include "StringView.h"
#include <cassert>
using namespace std;
int main()
{
const char data[] = "hello world!";
StringView str1(data); StringView str2(data + 6, 6);
assert(str1 != str2); assert(str1 > StringView("abc")); assert(str1 < StringView("xyz"));
return 0;
}
A view over part of a wide character string
And while we're at it, let's throw in a WStringView
class for good measure. It's almost identical to the StringView
class anyway.
#pragma once
#include "ArrayView.h"
#include <string>
struct WStringView : ArrayView<wchar_t>
{
WStringView()
{
}
WStringView(const wchar_t *const pStr, const size_t length) :
ArrayView(pStr, length)
{
}
WStringView(const wchar_t *const pStr) :
ArrayView(pStr, wcslen(pStr))
{
}
std::wstring str() const
{
return std::wstring(m_pData, m_length);
}
};
And here's some code which uses it.
#include "WStringView.h"
#include <cassert>
using namespace std;
int main()
{
const wchar_t data[] = L"hello world!";
WStringView str1(data); WStringView str2(data + 6, 6);
assert(str1 != str2); assert(str1 > WStringView(L"abc")); assert(str1 < WStringView(L"xyz"));
return 0;
}
Storage in standard containers
ArrayView
, StringView
and WStringView
lend themselves to be easily stored in standard containers. The following example demonstrates storing StringView
objects as keys in an std::map
.
#include "StringView.h"
#include <map>
#include <iostream>
using namespace std;
int main()
{
map<StringView, int> cost
{
{ "grape", 10, }, { "banana", 20, },
{ "apple", 30 },
};
for(const auto &entry: cost)
cout << entry.first.str() << ": " << entry.second << endl;
return 0;
}
Closing
The source code has been tested with Microsoft Visual C++ 2013.
There is much potential for improvement; if you make changes to the code, improve it, or have some better ideas, I would love to know. I can be reached by email at francisxavierjp [at] gmail [dot] com. Comments and suggestions are always welcome!