ArrayView, StringView






4.64/5 (12 votes)
Immutable, non-owning, efficient views over all or part of existing arrays and strings.
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()
{
// Array of existing data
const vector<int> arr = { 1, 2, 3, 4, 5 };
// Copy a subset of the data
const vector<int> subset(arr.data() + 1, arr.data() + 4);
// subset is now 2, 3, 4
// Pass the copy
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()
{
// Array of existing data
const vector<int> arr = { 1, 2, 3, 4, 5 };
Func(
arr.data() + 1, // Pointer to first element
3 // Number of elements to use
);
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()
{
// Array of existing data
const vector<int> arr = { 1, 2, 3, 4, 5 };
Func(ArrayView<int>(
arr.data() + 1, // Pointer to first element
3 // Number of elements to use
));
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) instd::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:
// Default constructor
ArrayView() :
m_pData(nullptr),
m_length(0)
{
}
// Constructor
ArrayView(const Ptr pData, const size_t length) :
m_pData(pData),
m_length(length)
{
}
// Iterators
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; }
// Capacity
bool empty() const { return m_length == 0; }
size_t size() const { return m_length; }
// Element access
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");
}
// Modifiers
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; } }
// Relational operators
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); // So arr1 is 1, 2, 3
ArrayView<int> arr2(data + 2, 3); // And arr2 is 3, 4, 5
assert(arr1 != arr2); // Obviously they can't be equal
assert(arr1 < arr2); // arr1 is lexicographically less than arr2
assert(!arr1.empty()); // Surely arr1 can't be empty
assert(arr1.size() == 3); // Yes, 3 elements is what we expect
assert(arr1[0] == 1); // Let's make sure that the right elements are present
assert(arr1[1] == 2);
assert(arr1[2] == 3);
arr1.swap(arr2); // Same as doing swap(arr1, arr2);
assert(arr1[0] == 3); // Now arr1 has the elements arr2 had.
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>
{
// Default constructor
StringView()
{
}
// Constructor
StringView(const char *const pStr, const size_t length) :
ArrayView(pStr, length)
{
}
// Convenience constructor
StringView(const char *const pStr) :
ArrayView(pStr, strlen(pStr))
{
}
// Get standard string
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); // So str1 is "hello world!"
StringView str2(data + 6, 6); // And str2 is only "world!"
assert(str1 != str2); // They're not equal
assert(str1 > StringView("abc")); // lexicographically greater
assert(str1 < StringView("xyz")); // lexicographically less
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>
{
// Default constructor
WStringView()
{
}
// Constructor
WStringView(const wchar_t *const pStr, const size_t length) :
ArrayView(pStr, length)
{
}
// Convenience constructor
WStringView(const wchar_t *const pStr) :
ArrayView(pStr, wcslen(pStr))
{
}
// Get standard string
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); // So str1 is "hello world!"
WStringView str2(data + 6, 6); // And str2 is only "world!"
assert(str1 != str2); // They're not equal
assert(str1 > WStringView(L"abc")); // lexicographically greater
assert(str1 < WStringView(L"xyz")); // lexicographically less
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, }, // Does not allocate additional memory for the strings!
{ "banana", 20, },
{ "apple", 30 },
};
for(const auto &entry: cost)
cout << entry.first.str() << ": " << entry.second << endl;
// Expected output:
// apple: 30
// banana: 20
// grape: 10
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!