Sim­i­lar­ly to the point­er and ref­er­ence wrap­pers de­scribed in the last ar­ti­cle , Mag­num’s ar­ray views re­cent­ly re­ceived STL com­pat­i­bil­i­ty as well. Let’s take that as an op­por­tu­ni­ty to com­pare them with the stan­dard im­ple­men­ta­tion in std::span .

This was meant to be a short blog post show­ing the new STL com­pat­i­bil­i­ty of var­i­ous Ar­rayView class­es. How­ev­er, af­ter div­ing deep in­to std::span, there was sud­den­ly much more to write about.

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­bi­lized, 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 process it re­ceived a fixed-size vari­ant called Con­tain­ers::Stati­cAr­rayView and, most re­cent­ly, Con­tain­ers::StridedAr­rayView , for eas­i­er it­er­a­tion over sparse da­ta. I’ll be show­ing its sparse mag­ic in a lat­er ar­ti­cle.

Ar­ray views were un­doubte­ly one of the main work­flow-defin­ing struc­tures since they were added to Mag­num al­most six years ago ; back then called ArrayReference , as I didn’t dis­cov­er the idea of slic­ing them yet. Sud­den­ly it was no longer need­ed 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 won­dered how long it would take be­fore the stan­dard in­tro­duces an equiv­a­lent, al­low­ing me to get rid of my own in fa­vor of a well-test­ed, well-op­ti­mized and well-known im­ple­men­ta­tion.

There was one ad­di­tion­al in­con­sis­ten­cy — for­tu­nate­ly cor­rect­ed since — where std::span was the on­ly type that used signed std::ptrdif­f_t in­stead of std::size_t as an in­dex type. This caused a lot of un­nec­es­sary fric­tion and was re­vert­ed with P1227 . Cor­re­spond­ing changes in Clang’s libc++ will prob­a­bly ship with Clang 9 .

To add more salt to the wound, C++17 has std::string_view (so not std::string_span ) and is a const view. So much for the con­sis­ten­cy.

The std::span was orig­i­nal­ly named std::array_view , which to me per­son­al­ly con­veys the mean­ing of a view on a con­tigu­ous mem­o­ry range much bet­ter. How­ev­er, we end­ed up with a span, be­cause it seems the com­mit­tee felt like call­ing it a span that day . To­geth­er with lump­ing both dy­nam­ic and stat­i­cal­ly-sized views in­to one bag, I fear it on­ly makes such a sim­ple type hard­er to teach and rea­son about.

Orig­i­nal­ly, std::span was meant to not on­ly han­dle both dy­nam­ic and fixed-size ar­ray views, but al­so mul­ti-di­men­sion­al and strid­ed views. For­tu­nate­ly such func­tion­al­i­ty was sep­a­rat­ed in­to std::mdspan , to ar­rive prob­a­bly no ear­li­er than in C++23 (again, way too late). If you want to know more about mul­ti-di­men­sion­al ar­ray views and how they com­pare to the pro­posed stan­dard con­tain­ers, have a look at Mul­ti-di­men­sion­al strid­ed ar­ray views in Mag­num .

In­stead of ship­ping a min­i­mal vi­able im­ple­men­ta­tion as soon as pos­si­ble to get code­bas­es jump on it — and let its fu­ture de­sign adapt to us­er feed­back — de­sign-in-a-vac­u­um means C++2a will ship with a com­plex im­ple­men­ta­tion and a set of gude­lines that users have to adapt to in­stead.

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

So, what’s Con­tain­ers::Ar­rayView ca­pa­ble of? Like std::span, it can be im­plic­it­ly con­struct­ed from a C ar­ray ref­er­ence, or ex­plic­it­ly from a pair of point­er and a size. It’s al­so pos­si­ble to slice the ar­ray, equiv­a­lent­ly 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­i­lar­ly it goes for stat­i­cal­ly-sized ar­ray views. It’s pos­si­ble to con­vert be­tween dy­nam­i­cal­ly-sized and stat­i­cal­ly-sized ar­ray views us­ing fixed-size slice<n>() and re­lat­ed 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­for­mance rea­sons, the el­e­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­plic­it­ly con­vert­ible to point­ers in­stead of pro­vid­ing cus­tom it­er­a­tors or an operator[] ). On the oth­er hand, slic­ing is checked, so it­er­at­ing over a slice is pre­ferred over man­u­al­ly 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 Python-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 al­ready at com­pile time.

As @zeux­cg right­ful­ly point­ed out on Twit­ter, not sup­ply­ing an operator [] doesn’t re­al­ly help de­bug per­for­mance for ran­dom ac­cess, since the operator T * has to be called in­stead. Both are func­tion calls and have the same over­head in de­bug builds. This al­so means ar­ray views might get checked operator [] at some point as well, howew­er it’ll prob­a­bly be opt-in to avoid as­ser­tion mes­sages get­ting in­lined in ev­ery place where the func­tion gets called.

STL com­pat­i­bil­i­ty Con­tin­u­ing with how Con­tain­ers::Point­er, Con­tain­ers::Ref­er­ence and Con­tain­ers::Op­tion­al re­cent­ly be­came con­vert­ible from/to std::unique_p­tr, std::ref­er­ence_wrap­per and std::op­tion­al; ar­ray views now ex­pose a sim­i­lar func­tion­al­i­ty. The Con­tain­ers::Ar­rayView can be im­plic­it­ly cre­at­ed from a std::vec­tor or an std::ar­ray ref­er­ence, plus Con­tain­ers::Stati­cAr­rayView can be im­plic­it­ly con­vert­ed from the (fixed-size) std::ar­ray. All you need to do is in­clud­ing the Cor­rade/Con­tain­ers/Ar­rayView­Stl.h head­er to get the con­ver­sion def­i­ni­tions. Sim­i­lar­ly as men­tioned in the pre­vi­ous ar­ti­cle, it’s a sep­a­rate head­er to avoid un­con­di­tion­al heavy #include <vector> and #include <array> be­ing tran­si­tive­ly present in all code that touch­es ar­ray views. With that in place, you can do things like the fol­low­ing — with slic­ing prop­er­ly bounds-checked, but no fur­ther over­head re­sult­ing from it­er­a­tor or el­e­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 stan­dard C++2a std::span in­stead (or you in­ter­face with a li­brary us­ing it), there’s no need to wor­ry ei­ther. A com­pat­i­bil­i­ty with it is pro­vid­ed in Cor­rade/Con­tain­ers/Ar­rayView­StlSpan.h. As far as I’m aware, on­ly libc++ ships an im­ple­men­ta­tion of it at the mo­ment. For the span there’s many more dif­fer­ent con­ver­sion pos­si­bil­i­ties, see the docs for more in­for­ma­tion. This con­ver­sion is again sep­a­rate 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­si­ble for just a fan­cy pair of point­er and size with a hand­ful of one-lin­er mem­ber func­tions to be that big, but here we are.

Ar­ray cast­ing When work­ing with graph­ics da­ta, you of­ten end up with a non-de­script “ar­ray of bytes”, com­ing from ei­ther some file for­mat or be­ing down­load­ed from the GPU. Be­ing able to rein­ter­pret them as a con­crete type is of­ten very de­sired and Mag­num pro­vides Con­tain­ers::ar­ray­Cast() for that. Be­sides change of type, it al­so prop­er­ly re­cal­cu­lates the size to cor­re­spond to the new type. Containers :: ArrayView < char > data ; auto positions = Containers :: arrayCast < Vector3 > ( data ); // array of Vector3 Apart from the con­ve­nience, its main pur­pose is to di­rect the reinterpret_cast<> ma­chine gun away from your feet. While it can’t ful­ly stop it from fir­ing, it’ll check that both types are stan­dard lay­out (so with­out vta­bles and oth­er fun­ny busi­ness), that one type has its size a mul­ti­ple of the oth­er and that the to­tal byte size of the view doesn’t change af­ter the cast. That al­lows you to do fanci­er things as well, such as rein­ter­pret­ing an ar­ray of Ma­trix3 in­to an ar­ray of its col­umn 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. But, but… strict aliasing?! C++ purists may right­ful­ly point out that do­ing the above is an un­de­fined be­hav­ior, break­ing strict alias­ing rules. That’s cor­rect. What is al­so cor­rect is that nei­ther std::mdspan can be im­ple­ment­ed clean­ly with­out hit­ting any un­de­fined be­hav­ior. The case of std::mdspan was ap­par­ent­ly solved by abus­ing a “le­gal loop­hole” — the sole pres­ence of a type in stan­dard li­brary means there’s no un­de­fined be­hav­ior in its im­ple­men­ta­tion. More­over, stan­dard li­brary types don’t have to be im­ple­mentable out­side of the stan­dard li­brary. I per­son­al­ly refuse to ac­cept such sta­tus quo, so both the Con­tain­ers::ar­ray­Cast() and the Con­tain­ers::StridedAr­rayView will stay and I’ll wait for the lan­guage to fix it­self in­stead.

Type era­sure Com­ple­men­tary to the cast­ing func­tion­al­i­ty, some APIs in Mag­num ac­cept ar­ray views with­out re­quir­ing any par­tic­u­lar type — var­i­ous GPU da­ta up­load func­tions, im­age views and so on. Such APIs care on­ly about the da­ta point­er and byte size. A Con­tain­ers::Ar­rayView<con­st void> spe­cial­iza­tion is used for such case and to make it pos­si­ble to pass in ar­ray views of any type, it’s im­plic­it­ly con­vert­ible from them, with their size get­ting re­cal­cu­lat­ed to byte count. Look­ing at std::span, it pro­vides some­thing sim­i­lar through std::as_bytes(), how­ev­er it’s an ex­plic­it op­er­a­tion and is us­ing the fan­cy new std::byte type (which, in my opin­ion, doesn’t add any­thing use­ful over the sim­i­lar­ly opaque void* ) — and al­so, due to that, is not constexpr (while the Mag­num ar­ray view type era­sure is).

Point­er-like se­man­tics Mag­num’s ar­ray views were de­lib­er­ate­ly cho­sen to have se­man­tics sim­i­lar to C ar­rays — they’re im­plic­it­ly con­vert­ible to its un­der­ly­ing point­er type (which, again, al­lows us to op­ti­mize de­bug per­for­mance by not hav­ing to ex­plic­it­ly pro­vide operator[] ) and the usu­al point­er arith­metic works on them as well. That al­lows them to be more eas­i­ly used when in­ter­fac­ing with C APIs, for ex­am­ple like be­low. The std::span doesn’t ex­pose any such func­tion­al­i­ty. Containers :: ArrayView < const void > data ; std :: FILE * file ; std :: fwrite ( data , 1 , data . size (), file ); The point­er-like se­man­tics means al­so that operator== and oth­er com­par­i­son op­er­a­tors work the same way as on point­ers. Ac­cord­ing to cp­pref­er­ence at least, std::span doesn’t pro­vide any of these and since it doesn’t re­tain any­thing else from the point­er-like se­man­tics, it’s prob­a­bly for the bet­ter — since std::span has nei­ther re­al­ly a point­er nor a con­tain­er se­man­tics, both rea­sons for == be­hav­ior like on a point­er or like on a con­tain­er are equal­ly valid for ei­ther par­ty and equal­ly con­fus­ing for the oth­er.