Array view implementations in Magnum

Sim­il­arly to the point­er and ref­er­ence wrap­pers de­scribed in the last art­icle, Mag­num’s ar­ray views re­cently re­ceived STL com­pat­ib­il­ity as well. Let’s take that as an op­por­tun­ity to com­pare them with the stand­ard im­ple­ment­a­tion in std::span.

This was meant to be a short blog post show­ing the new STL com­pat­ib­il­ity of vari­ous Ar­rayView classes. How­ever, after diving deep in­to std::span, there was sud­denly much more to write about.

The story of wait­ing for a thing to get stand­ard­ized…

Ar­ray views were un­doubtely one of the main work­flow-de­fin­ing struc­tures since they were ad­ded to Mag­num al­most six years ago; back then called ArrayReference, as I didn’t dis­cov­er the idea of sli­cing them yet. Sud­denly it was no longer needed to pass ref­er­ences to std::vec­tor / std::ar­ray around, or — the hor­ror — a point­er and a size. Back then, still in the brave new C++11 world, I wondered how long it would take be­fore the stand­ard in­tro­duces an equi­val­ent, al­low­ing me to get rid of my own in fa­vor of a well-tested, well-op­tim­ized and well-known im­ple­ment­a­tion.

Fast for­ward to 2019, we might soon be there with std::span, sched­uled for in­clu­sion in C++2a. In the mean­time, Mag­num’s Con­tain­ers::Ar­rayView sta­bil­ized, learned from its past mis­takes and was used in so many con­texts that I dare to say it’s fea­ture-com­plete. In the pro­cess it re­ceived a fixed-size vari­ant called Con­tain­ers::StaticAr­rayView and, most re­cently, Con­tain­ers::StridedAr­rayView, for easi­er it­er­a­tion over sparse data. I’ll be show­ing its sparse ma­gic in a later art­icle.

… and ul­ti­mately real­iz­ing it’s not really what we want

Much like std::op­tion­al, ori­gin­ally sched­uled for C++11 but due to its design be­com­ing more and more com­plex (constexpr sup­port, op­tion­al ref­er­ences, …), caus­ing it to be delayed un­til C++17; std::span is, in my opin­ion, ar­riv­ing way too late as well.

In­stead of ship­ping a min­im­al vi­able im­ple­ment­a­tion as soon as pos­sible to get code­bases jump on it — and let its fu­ture design ad­apt to user feed­back — design-in-a-va­cu­um means C++2a will ship with a com­plex im­ple­ment­a­tion and a set of gudelines that users have to ad­apt to in­stead.

In short, the C++2a std::span provides:

  • the usu­al in­dex-based and iter­at­or ac­cess to ele­ments of the view,
  • both dy­nam­ic-size and fixed-size ar­ray views in a single type (which, as I un­for­tu­nately soon real­ized, only com­plic­ates everything without hav­ing any real be­ne­fits)
  • im­pli­cit con­ver­sion from C-style ar­rays, std::ar­ray and std::vec­tor,
  • and a well-meant, but fun­da­ment­ally broken im­pli­cit con­ver­sion from any type that con­tains a data() and a size() mem­ber. If that sounds dan­ger­ous, it’s be­cause it really is. More on that be­low.

Ori­gin­ally, std::span was meant to not only handle both dy­nam­ic and fixed-size ar­ray views, but also multi-di­men­sion­al and strided views. For­tu­nately such func­tion­al­ity was sep­ar­ated in­to std::md­span, to ar­rive prob­ably no earli­er than in C++23 (again, way too late). If you want to know more about multi-di­men­sion­al ar­ray views and how they com­pare to the pro­posed stand­ard con­tain­ers, have a look at Multi-di­men­sion­al strided ar­ray views in Mag­num.

Mag­num’s ar­ray views

So, what’s Con­tain­ers::Ar­rayView cap­able of? Like std::span, it can be im­pli­citly con­struc­ted from a C ar­ray ref­er­ence, or ex­pli­citly from a pair of point­er and a size. It’s also pos­sible to slice the ar­ray, equi­val­ently to std::span::sub­span() and friends:

float data[] { 1.0f, 4.2f, 133.7f, 2.4f };
Containers::ArrayView<float> a = data;

// Multiply the first three items 10 times
for(float& i: a.prefix(3)) i *= 10.0f;

Sim­il­arly it goes for stat­ic­ally-sized ar­ray views. It’s pos­sible to con­vert between dy­nam­ic­ally-sized and stat­ic­ally-sized ar­ray views us­ing fixed-size slice<n>() and re­lated APIs — again, std::span has that too:

// Implicit conversion allowed only if data has 4 elements as well
Containers::StaticArrayView<float, 4> b = data;

// A function accepting a view on exactly three floats
float min3(Containers::StaticArrayView<float, 3>) { ... }

float min = min3(b.suffix<3>());

For de­bug per­form­ance reas­ons, the ele­ment ac­cess is not bounds-checked (in fact, to re­duce the it­er­a­tion over­head even more, the views are im­pli­citly con­vert­ible to point­ers in­stead of provid­ing cus­tom iter­at­ors or an operator[]). On the oth­er hand, sli­cing is checked, so it­er­at­ing over a slice is pre­ferred over manu­ally cal­cu­lat­ing an in­dex sub­range and in­dex­ing that way. If you step over with your slice, you’ll get a de­tailed Py­thon-like as­ser­tion mes­sage:

a.slice(3, 7);
Containers::ArrayView::slice(): slice [3:7] out of range for 4 elements

Of course, fixed-size slices on fixed-size ar­ray views are checked already at com­pile time.

STL com­pat­ib­il­ity

Con­tinu­ing with how Con­tain­ers::Point­er, Con­tain­ers::Ref­er­en­ce and Con­tain­ers::Op­tion­al re­cently be­came con­vert­ible from/to std::unique_ptr, std::ref­er­en­ce_wrap­per and std::op­tion­al; ar­ray views now ex­pose a sim­il­ar func­tion­al­ity. The Con­tain­ers::Ar­rayView can be im­pli­citly cre­ated from a std::vec­tor or an std::ar­ray ref­er­ence, plus Con­tain­ers::StaticAr­rayView can be im­pli­citly con­ver­ted from the (fixed-size) std::ar­ray. All you need to do is in­clud­ing the Cor­rade/Con­tain­ers/Ar­rayViewStl.h head­er to get the con­ver­sion defin­i­tions. Sim­il­arly as men­tioned in the pre­vi­ous art­icle, it’s a sep­ar­ate head­er to avoid un­con­di­tion­al heavy #include <vector> and #include <array> be­ing trans­it­ively present in all code that touches ar­ray views. With that in place, you can do things like the fol­low­ing — with sli­cing prop­erly bounds-checked, but no fur­ther over­head res­ult­ing from iter­at­or or ele­ment ac­cess:

#include <Corrade/Containers/ArrayViewStl.h>



std::vector<float> data;

float sum{}; // Sum of the first 100 elements
for(float i: Containers::arrayView(data).prefix(100))
    sum += i;

In case you’re feel­ing like us­ing the stand­ard C++2a std::span in­stead (or you in­ter­face with a lib­rary us­ing it), there’s no need to worry either. A com­pat­ib­il­ity with it is provided in Cor­rade/Con­tain­ers/Ar­rayViewStlSpan.h. As far as I’m aware, only libc++ ships an im­ple­ment­a­tion of it at the mo­ment. For the span there’s many more dif­fer­ent con­ver­sion pos­sib­il­it­ies, see the docs for more in­form­a­tion. This con­ver­sion is again sep­ar­ate from the rest be­cause (at least the libc++) #include <span> man­aged to gain al­most twice the weight as both #include <vector> and #include <array> to­geth­er. I don’t know how’s that pos­sible for just a fancy pair of point­er and size with a hand­ful of one-liner mem­ber func­tions to be that big, but here we are.

Ar­ray cast­ing

When work­ing with graph­ics data, you of­ten end up with a non-descript “ar­ray of bytes”, com­ing from either some file format or be­ing down­loaded from the GPU. Be­ing able to re­in­ter­pret them as a con­crete type is of­ten very de­sired and Mag­num provides Con­tain­ers::ar­rayCast() for that. Be­sides change of type, it also prop­erly re­cal­cu­lates the size to cor­res­pond to the new type.

Containers::ArrayView<char> data;
auto positions = Containers::arrayCast<Vector3>(data); // array of Vector3

Apart from the con­veni­ence, its main pur­pose is to dir­ect the reinterpret_cast<> ma­chine gun away from your feet. While it can’t fully stop it from fir­ing, it’ll check that both types are stand­ard lay­out (so without vt­ables and oth­er funny busi­ness), that one type has its size a mul­tiple of the oth­er and that the total byte size of the view doesn’t change after the cast. That al­lows you to do fan­ci­er things as well, such as re­in­ter­pret­ing an ar­ray of Mat­rix3 in­to an ar­ray of its column vec­tors:

Containers::ArrayView<Matrix3> poses;
auto baseVectors = Containers::arrayCast<Vector3>(poses);

Note that a cast of the poses to Vec­tor4 would not be per­mit­ted by the checks above. Which is a good thing.

Type eras­ure

Com­ple­ment­ary to the cast­ing func­tion­al­ity, some APIs in Mag­num ac­cept ar­ray views without re­quir­ing any par­tic­u­lar type — vari­ous GPU data up­load func­tions, im­age views and so on. Such APIs care only about the data point­er and byte size. A Con­tain­ers::Ar­rayView<const void> spe­cial­iz­a­tion is used for such case and to make it pos­sible to pass in ar­ray views of any type, it’s im­pli­citly con­vert­ible from them, with their size get­ting re­cal­cu­lated to byte count.

Look­ing at std::span, it provides some­thing sim­il­ar through std::as­_­bytes(), how­ever it’s an ex­pli­cit op­er­a­tion and is us­ing the fancy new std::byte type (which, in my opin­ion, doesn’t add any­thing use­ful over the sim­il­arly opaque void*) — and also, due to that, is not constexpr (while the Mag­num ar­ray view type eras­ure is).

Point­er-like se­mantics

Mag­num’s ar­ray views were de­lib­er­ately chosen to have se­mantics sim­il­ar to C ar­rays — they’re im­pli­citly con­vert­ible to its un­der­ly­ing point­er type (which, again, al­lows us to op­tim­ize de­bug per­form­ance by not hav­ing to ex­pli­citly provide operator[]) and the usu­al point­er arith­met­ic works on them as well. That al­lows them to be more eas­ily used when in­ter­fa­cing with C APIs, for ex­ample like be­low. The std::span doesn’t ex­pose any such func­tion­al­ity.

Containers::ArrayView<const void> data;
std::FILE* file;
std::fwrite(data, 1, data.size(), file);

The point­er-like se­mantics means also that operator== and oth­er com­par­is­on op­er­at­ors work the same way as on point­ers. Ac­cord­ing to cp­pref­er­ence at least, std::span doesn’t provide any of these and since it doesn’t re­tain any­thing else from the point­er-like se­mantics, it’s prob­ably for the bet­ter — since std::span has neither really a point­er nor a con­tain­er se­mantics, both reas­ons for == be­ha­vi­or like on a point­er or like on a con­tain­er are equally val­id for either party and equally con­fus­ing for the oth­er.

Sized null views

While this seemed like an ugly wart at first, I have to ad­mit the whole API be­came more con­sist­ent with such fea­ture in place. It’s about the pos­sib­il­ity to have a view on a nullptr, but with a non-zero size at­tached. This se­mantics is used, among oth­er things, by a few OpenGL APIs, where passing a null point­er to­geth­er with a size will cause a buf­fer or tex­ture to be al­loc­ated but with con­tents un­ini­tial­ized. To do this, it seemed more nat­ur­al to al­low sized ar­ray views be cre­ated from nullptr than to add ded­ic­ated APIs for preal­loc­a­tion. The fol­low­ing will preal­loc­ate a GPU buf­fer to 384 bytes:

GL::Buffer buffer;
buffer.setData({nullptr, 32*3*sizeof(float)});

Later, when adding Con­tain­ers::StaticAr­rayView, this fea­ture al­lowed me to provide it with an im­pli­cit con­struct­or. When check­ing out std::span, I dis­covered that im­pli­cit con­struct­or of the fixed-size vari­ant is not pos­sible.

Containers::StaticArrayView<16, float> a;   // {nullptr, 16}
//std::span<float, 16> b;                   // doesn't compile :(

Now, let’s see those un­for­giv­ing num­bers

Be­low is the usu­al graph of pre­pro­cessed line count for each head­er, gen­er­ated us­ing the fol­low­ing com­mand with GCC 8.2. At the time of writ­ing, lib­stdc++ doesn’t ship with <span> yet, so it’s ex­cluded from the com­par­is­on. To have more data, there com­par­is­on in­cludes gsl::span im­ple­ment­a­tion from Mi­crosoft’s Guideline Sup­port Lib­rary (ver­sion 2.0.0, re­quir­ing at least C++14) and nostd::span aka Span Lite 0.4.0 from Mar­tin Moene. As said be­fore, while pre­pro­cessed line count is not the only factor af­fect­ing com­pile times, it cor­rel­ates with it pretty well.

echo "#include <vector>" | gcc -std=c++11 -P -E -x c++ - | wc -l
2451.0 lines 8608.0 lines 12029.0 lines 15117.0 lines 0.0 lines 30715.0 lines 17607.0 lines 0 5000 10000 15000 20000 25000 30000 lines <Containers/ArrayView.h> <vector> <array> <vector> + <array> <span> <gsl/span> <span.hpp> N/A C++14 Preprocessed line count, GCC 8.2, C++11

std::span ships in Clang’s libc++ 7.0 (and thus I as­sume in Xcode 10.0 as well), so here’s a com­par­is­on us­ing libc++. To make the com­par­is­on fair, it uses the C++2a stand­ard in all cases:

echo "#include <span>" | clang++ -std=c++2a -stdlib=libc++ -P -E -x c++ - | wc -l
5954.0 lines 28147.0 lines 23632.0 lines 28512.0 lines 24098.0 lines 24456.0 lines 24178.0 lines 0 5000 10000 15000 20000 25000 lines <Containers/ArrayView.h> <vector> <array> <vector> + <array> <span> <gsl/span> <span.hpp> Preprocessed line count, Clang 7.0, libc++, C++2a

The Mag­num im­ple­ment­a­tion needs <type_traits> to do a bunch of SFINAE and com­pile-time checks, <utility> is needed for the std::for­ward() util­ity. While <utility> is com­par­at­ively easy to re­place, I still don’t think writ­ing my own type traits head­ers is worth the time in­vest­ment, mainly due to all the com­piler ma­gic that needs to be dif­fer­ent for each plat­form.

Com­pile times

To get some real tim­ing, I com­posed a tiny “mi­crobench­mark” shown be­low, with equi­val­ent vari­ants for STL span, GSL span and span lite, us­ing both GCC 8.2 in C++11 mode and Clang 7.0 with libc++ in C++2a mode. Like in the pre­vi­ous art­icle, to bal­ance the com­par­is­on, I’m switch­ing to the stand­ard as­ser­tions by de­fin­ing COR­RADE_STAND­AR­D_ASSERT and, for bet­ter sense of scale, there’s also a baseline time, which is from com­pil­ing just int main() {} with no #include at all.

#include <Corrade/Containers/ArrayView.h>

using namespace Corrade;

int main() {
    int data[]{1, 3, 42, 1337};

    auto a = Containers::arrayView(data);
    Containers::StaticArrayView<1, int> b = a.slice<1>(2);
    return b[0] - 42;
}
g++ main.cpp -DCORRADE_STANDARD_ASSERT -std=c++11                    # either
clang++ main.cpp -DCORRADE_STANDARD_ASSERT -std=c++2a -stdlib=libc++ # or
55.39 ± 2.47 ms 82.79 ± 6.78 ms 0.0 ± 0.0 ms 336.48 ± 14.49 ms 196.33 ± 4.19 ms 0 50 100 150 200 250 300 350 ms baseline Containers::ArrayView std::span gsl::span nonstd::span int main() {} N/A C++14 Compilation time, GCC 8.2, C++11
71.61 ± 3.28 ms 127.44 ± 3.81 ms 257.8 ± 6.56 ms 253.43 ± 3.73 ms 248.97 ± 5.23 ms 0 50 100 150 200 250 ms baseline Containers::ArrayView std::span gsl::span nonstd::span Compilation time, Clang 7.0, libc++, C++2a

De­bug per­form­ance

Look­ing at the size of as­sembly out­put for an un­op­tim­ized ver­sion of the snip­pet above, the Mag­num im­ple­ment­a­tion is 1/3 smal­ler than equi­val­ent code writ­ten with Span Lite and about three times smal­ler than the same us­ing GSL span. In all cases the com­piler is able to op­tim­ize everything away at -O1. Un­for­tu­nately Com­piler Ex­plorer doesn’t have an op­tion to use libc++, so couldn’t make a com­par­is­on with std::span there.

The baby steps (and falls) of std::span

If you sur­vived all the way down here without ab­ruptly leav­ing with an ir­res­ist­ible urge to re­write everything in Rust be­come a barista in­stead, you’d think it stops just at aw­ful com­pile times. Well, no. It’s worse than that.

Hot take: im­pli­cit all-catch­ing con­struct­ors are stu­pid

I dis­covered the first is­sue when writ­ing the STL com­pat­ib­il­ity con­ver­sions. All Mag­num con­tain­ers and math types have a spe­cial con­struct­or and a con­ver­sion op­er­at­or that makes it pos­sible to con­vert a type either ex­pli­citly or — if the type is simple enough, con­ver­sion not costly and there are no risks of caus­ing am­bigu­ous op­er­at­or over­loads — im­pli­citly from and to a third-party type. This way Mag­num sup­ports seam­less us­age its math types with GLM, Bul­let Phys­ics, Vulkan types or, for ex­ample, Dear ImGui.

This works well and causes no prob­lem as long as the third-party type doesn’t have a con­struct­or that ac­cepts any­thing you throw at it. I ran in­to this is­sue two weeks ago with Ei­gen, as both its Array and Matrix classes have such a con­struct­or. But in that case it’s not harm­ful, only an­noy­ing, as the con­ver­sion can no longer be done dir­ectly through an ex­pli­cit con­ver­sion but rather us­ing some con­ver­sion func­tion.

In case of std::span, it’s much worse — there’s an all-catch­ing con­struct­or tak­ing any con­tain­er-like type. It’s a well meant fea­ture, how­ever, it works even in the case of a fixed-size span — and there it gets dan­ger­ous, as shown be­low. And this is not just a cause of an im­ple­ment­a­tion is­sue in libc++, it’s de­signed this way in the stand­ard it­self — of all things (ex­cep­tions, as­serts, com­pile-time er­rors), it chooses the worst — such con­ver­sion is de­clared as un­defined be­ha­vi­or. For­tu­nately, the good people of Twit­ter already re­cog­nized this as a de­fect and are work­ing on a solu­tion. Hope­fully the fix gets in to­geth­er with the span and not tree years later or some­thing.

#include <span>

struct Vec3 { // your usual Vec3 class
    size_t size() const { return 3; }
    float* data() { return _data; }
    const float* data() const { return _data; }

    private: float _data[3]{};
};

int main() {
    Vec3 a;
    std::span<float, 57> b = a; // this compiles?!?!
}

Im­pli­cit con­ver­sion from std::ini­tial­izer­_l­ist is act­ively har­fmul

Some time ago there was a Twit­ter dis­cus­sion where it was sug­ges­ted to add a con­struct­or tak­ing std::ini­tial­izer­_l­ist to an ar­ray view class. I wondered why Mag­num’s Con­tain­ers::Ar­rayView class doesn’t have such an use­ful fea­ture … un­til I re­membered why. Con­sider this in­no­cent-look­ing snip­pet, guess what hap­pens when you ac­cess b[0] later? If you don’t know, try again with -fsanitize=address.

std::span<const std::string> b{{"hello", "there"}};
b[0]; // ?

Thing is, the above-men­tioned all-catch­ing con­struct­or can cap­ture an std::ini­tial­izer­_l­ist as well, how­ever the prob­lem (com­pared to, let’s say, do­ing the same with a std::vec­tor), is that it gets con­struc­ted im­pli­citly — and so it’s very hard to real­ize the ini­tial­izer list ele­ments are already des­troyed after the semi­colon.

In case of Mag­num, rather than hav­ing ar­ray views im­pli­citly con­struct­ible from std::ini­tial­izer­_l­ist, where it makes sense, APIs tak­ing an ar­ray view have also an ini­tial­izer list over­load. It makes the API sur­face lar­ger, but that’s a reas­on­able price to pay for ar­ray views be­ing safer to use.

Single-head­er im­ple­ment­a­tion

The Mag­num Singles re­pos­it­ory in­tro­duced pre­vi­ously got a new neigh­bor — all the ar­ray view classes, in a tiny, self-con­tained, de­pend­ency-less and fast-to-com­pile head­er file, meant to be bundled right in­to your pro­ject:

Lib­rary LoC Pre­pro­cessed LoC De­scrip­tion
Cor­rade­Ar­rayView.h new 558 2453 See Con­tain­ers::Ar­rayView and StaticAr­rayView docs
Cor­rade­Op­tion­al.h 328 2742 See Con­tain­ers::Op­tion­al docs
Cor­rade­Point­er.h 259 2321 See Con­tain­ers::Point­er docs
Cor­radeRefer­en­ce.h 115 1639 See Con­tain­ers::Ref­er­en­ce docs
Cor­ra­de­Scope­Guard.h 131 34 See Con­tain­ers::Scope­Guard docs

Funny thing is, even though the Con­tain­ers::Ar­rayView API is much lar­ger than of Con­tain­ers::Op­tion­al, it still boils down to less code after pre­pro­cessing — reas­on is simply that the <new> in­clude was not needed, since ar­ray views don’t do any fancy al­loc­a­tions.

* * *

Ques­tions? Com­plaints? Share your opin­ion on so­cial net­works: