Passing data using the Array_ref wrapper (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 170
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

Passing data using the Array_ref wrapper (C++)

Post by fips »

In C++, It's been always a bit of dilemma for me how to pass arrays, raw data and strings around, especially if they come from a single compiled BLOB (as is quite common in the context of game development) with a lifetime long enough that it outlives all the operations one needs to perform on it.

The traditional C way to pass (or more precisely refer) an array is by using the [*data, size] or [*begin, *end] pairs, although efficient, it's quite error prone and hard to maintain, moreover it makes code full of assertions in order to catch all the corner cases.

There's also a C++ way that employs a const reference to std::vector or std::string, although more atomic, it has its own downside in the form of an unnecessary initial copy that has to be made just to convert the raw data that is permanently present in the BLOB. That's why I generally don't like having const references to STL containers or strings in interfaces, but rather prefer using STL collections as type members, for storage only.

That brings us to the 3rd option, something called Array_ref (or String_ref), which basically provides a cheap mapping to the raw data nicely wrapped into a lightweight type with a rich STL-like interface. I'd been playing with this idea for some time but actually never managed to materialize it, but then came across llvm::ArrayRef and llvm::StringRef, and realized that it was exactly what I was looking for. So I've decided to give it a try and here's my sample implementation:

VIEW THE CODE BELOW IN FULL-SCREEN (array_ref_sample.cpp)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=813
*/

#include <vector>
#include <cstdint> // uint8_t, ...
#include <cassert>

/// A reference to an array (data mapper) with a rich STL-like interface, safe & cheep to pass around.
template <typename T>
class Array_ref
{
 public:

    Array_ref() : _data(nullptr), _size(0) {}
    Array_ref(const T &value) : _data(&value), _size(1) {}
    Array_ref(const T *begin, const T *end) : _data(begin), _size(end - begin) { assert(begin <= end); }
    Array_ref(const T *data, size_t size) : _data(data), _size(size) {}
    Array_ref(const std::vector<T> &vec) : _data(vec.empty() ? nullptr : &vec.front()), _size(vec.size()) {}

    bool empty() const { return _size == 0; }

    const T * begin() const { return _data; }
    const T * end() const { return _data + _size; }

    size_t size() const { return _size; }
    const T * data() const { return _data; }

    const T & front() const { assert(!empty()); return _data[0]; }
    const T & back() const { assert(!empty()); return _data[_size - 1]; }

    const T & operator [] (size_t index) const { assert(index < _size); return _data[index]; }

    //... other convenient stuff

 private:

    const T *_data;
    size_t _size;
};

/// A helper that conveniently translates a raw array to the Array_ref.
template <typename T, size_t N>
Array_ref<T> make_array_ref(const T (&array)[N]) // a reference to array is used here, to preserve N
{
    return Array_ref<T>(&array[0], &array[N]);
}

//--- DEMO ---

struct Color { uint8_t r, g, b; };
typedef Array_ref<Color> Color_array_ref;

/// A traditional way to refer data, but it's unsafe and requires 2 params to manage.
void pass_color_data_ugly1(const Color *data, size_t size)
{
    printf("pass_color_data_ugly1():\n");
    for(size_t i = 0; i < size; ++i)
    {
        const Color &c = data[i];
        printf("%02x %02x %02x\n", c.r, c.g, c.b);
    }
}

/// A traditional way to refer data, but it's unsafe and requires 2 params to manage.
void pass_color_data_ugly2(const Color *begin, const Color *end)
{
    printf("pass_color_data_ugly2():\n");
    for(const Color *c = begin; c < end; ++c)
    {
        printf("%02x %02x %02x\n", c->r, c->g, c->b);
    }
}

/// A safe C++ish way to refer data, but it requires an unnecessary initial copy :(
void pass_color_data_ugly3(const std::vector<Color> &data)
{
    printf("pass_color_data_ugly3():\n");
    for(std::vector<Color>::const_iterator c = data.begin(), end = data.end(); c < end; ++c)
    {
        printf("%02x %02x %02x\n", c->r, c->g, c->b);
    }
}

/// A safe way to refer data, no initial copy needed, it's just a mapping!
void pass_color_data_cool(Color_array_ref color_arr)
{
    printf("pass_color_data_cool():\n");
    for(size_t i = 0; i < color_arr.size(); ++i)
    {
        const Color &c = color_arr[i];
        printf("%02x %02x %02x\n", c.r, c.g, c.b);
    }
}

int main()
{
    // This represents raw data. It might be a raw array like this or just
    // a bunch of bytes, somewhere it the middle of a BLOB.
    const Color data[] = {
     { 0xff, 0x00, 0x00 }, { 0x00, 0xff, 0x00 },
     { 0x00, 0x00, 0xff }, { 0x00, 0x00, 0x00 }
    };

    pass_color_data_ugly1(&data[0], sizeof(data) / sizeof(data[0]));
    pass_color_data_ugly2(&data[0], &data[sizeof(data) / sizeof(data[0])]);
    pass_color_data_ugly3(std::vector<Color>(&data[0], &data[sizeof(data) / sizeof(data[0])]));
    pass_color_data_cool(make_array_ref(data)); // Pure beauty, isn't it?

    return 0;
}

// output:
// pass_color_data_ugly1():
// ff 00 00
// 00 ff 00
// 00 00 ff
// 00 00 00
// pass_color_data_ugly2():
// ff 00 00
// 00 ff 00
// 00 00 ff
// 00 00 00
// pass_color_data_ugly3():
// ff 00 00
// 00 ff 00
// 00 00 ff
// 00 00 00
// pass_color_data_cool():
// ff 00 00
// 00 ff 00
// 00 00 ff
// 00 00 00