Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

ArrayView, StringView

4.64/5 (13 votes)
2 Dec 2014MIT3 min read 29.2K  
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
C++
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
C++
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
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) in std::map.

Because the implementation is really quite simple, without further ado, the ArrayView class implementation is presented below.

C++
#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.

C++
#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.

C++
#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.

C++
#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.

C++
#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.

C++
#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.

C++
#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!

License

This article, along with any associated source code and files, is licensed under The MIT License