C++ native array handling

C++ and modern C are two different languages. C++ branched from K&R C.  Today it is not based on modern C99, C11 and beyond.

There are C++ users, who truly believe native arrays are to be avoided in modern C++. I disagree. Another side of that coin is perhaps, somewhat like me, you might also believe no feature of modern c++ std lib, has to be used just because it exists, without knowing why.

Thus the scene is set. Two opposing groups of C++ practitioners. But they both agree: std::array is a “good thing”. They disagree when, how and why to use it.

What I think? When you think you need it, you better be sure it is the right tool to solve your problem and you be sure you use it properly too.  Enter: native array. Why does it exist C++ zealots might be bold to ask?  Let’s try and reason with them. For starters:

 

 

The native array is a foundation cornerstone

Of course, the native array is one of the cornerstones in the foundations of modern C and modern C++. Here is the diagram you have seen before, perhaps many times. But please stop and understand, this is first and foremost the diagram representing the C++ key concepts. This is not K&R C diagram.

Contiguous memory and iterators (begin & end), operating on it.

Iterators are conceptually the pointer to the first element of the contiguous memory (aka array) and a pointer to the  “one behind” (aka “+1”) address after the last element. Repeat: The end is last + 1.

And, next is yet another important detail many of you dear fellow C++ developers surely knew a long time ago. But the one not perhaps that much in your focus recently.

Key abstraction

T (&)[N]

That is a declaration of the reference to the array of size N,  I like to call it: NAtive aRray reFerence or abbreviated NARF.  In standard C, this does not exist. Closest to it we  have in  C is

T (*) [N]

Pointer to an array. A source of many confusions. Because C++ array decay does not transform array to that. Array decay is not transformation to the pointer to the array. Array decay is simply pointer to the first arrsay element.

Why is NARF “key abstraction”?

Most importantly, NARF is how C++ passes arrays into the functions and out of the functions.  Basically NARF is always used and pointer to array is never used.

One good reason NARF is important, is that native array reference, is  the type compiler uses for native arrays  used in the modern c++ range-based for loop,

This works because C++ compiler sees T(&)N  aka  (const char * (&)[4], above) and thus, in turn, knows the number N, of elements in the nativarr above.  In turn, a compiler can also deduce begin and end iterators to the NARF.

// for the abovecompiler 'knows'
auto  begin = nativarr ;
// one after the end
auto    end = nativarr + 4;

Hint: Remember the diagram on the top?

C and C++ native arrays can be passed in as value arguments

// C99
void take_array ( int size, char string[ size ] ) ;

That is legal C99. That is not C++. Also, we can not return arrays from functions.

C++ NARF can be passed as the return type

In C++ there is one a bit different way to return native arrays from functions. One can declare arguments as array references, and return them from functions, etc.

In standard C++ it is illegal to declare NARF (native array reference) to the array of unknown size. Hence the repeated usage of ‘256’ above.

Above is illegal and will not compile. Native array reference, size must be known at compile time. NARF and template are marriage made in heaven.

Thsat is the standard way. It works for any type T and any size N. It is generic. Amd obvious candidate for compile time.

It seems you are by now beginning to like native arrays in C++.

We have to talk about std::array

I know, some of you are itching on this one by now.  You want to shout out: Why not just use the std:array? !

The answer is simple: The integration.

std::array is a full-blown C++ std container. Very basic but still a container using one native array inside. NARF is not a container. NARF is a tiny abstraction representing a reference to the native array. NARF could be visualized as an atomic building block of larger abstractions.

And yes, there is no std::array in the land of Modern C.

Modern C

C++ is/was based on K&R C. Modern C too. C++ is not based on modern C.  This common heritage requires a lot of native arrays on both sides of the proverbial fence, between C run-time (CRT) and C++ above it. Just look into the MSVC STL source peppered with C files.  In there you will find a *lot* of very important C implementations operating on native arrays, aka contiguous memory. Very often using two native pointers representing array begin and end concepts, straight from the C++ space one level above. (Hint: lookup that diagram on top now)

To dance in and out of these C functions would be very complex dance without native arrays being possible in the modern C++ code.  C/C++ string literals are perhaps the prime example of that “dance”.

String comparisons are (mega) important, functions written in C.  Their declarations are very similar to these two as an example:

Now imagine using these from modern C++ but without native arrays,  and also without references to native arrays, at hand. I am sure you understand that might work but will be far inferior implementation to the fortunate reality of using native arrays in modern C++.

[nextpage title=”Using modern C++ to encapsulate the concept”]

dbj NARF

You might be a bit tired by now and thinking: Grand words, show me some code. Fine. Without any more delay, here is my key abstraction for dealing with native arrays in modern c++. It is a reference to the native array encapsulated using the std::reference_wrapper.  More precisely modern c++ template alias.

NARF. Yes, this funny acronym will perhaps make it easier to remember this key abstraction. dbj::narf::wrapper . So what is dbj narf?

dbj NARF contains (encapsulates) reference to native array T(&)[N]. NARF in itself cannot be safely copied and moved around. One, in essence, uses NARF to “carry around” references to native arrays. Array stays where it was made. That  makes the requirement to handle dbj NARF with care.

And perhaps the key reason of existence:  unlike std::array<T,N> , NARF readily delivers easy to use, reference to native array aka T(&)[N], it encapsulates.

By using dbj::narf functionality one can safely and practically deal with native arrays in a modern way in a modern C++.

Abstractions based on dbj NARF?  I am sure they will come. Like for example “array of references” which is illegal in modern c++. But easy to code by using dbj narf.

I know, one can do almost all with std::array, besides one key propblem@ easily getting to the reference of the native array it contains. But there is also one more broad subject that delivers a focused and convincing proof we need native arrays. As already mentioned, there is one very important land where std::array does not exist. Standard C.

dbj::narf

I have made a very small core of function helpers to make for comfortable usage of.dbj::narf:wrapperThis is functional programming. dbj::narf does not contain a single class.

Of course feel free to browse and wander around. The code is never enough on its own, but still, this one is pretty well documented. Next, I will focus on its perceived usage. (look into the DBJ_TEST_UNIT in the source.).  First few dbj::narf examples.

Make narf from std::array instance:

auto arf = dbj::narf::make(std::array<int, 10>{});

Make narf from init list:

// make ref to array of 10 int's
dbj::narf::make({ 0,1,2,3,4,5,6,7,8,9 });

Make narf from the native array of narrow  string literals

dbj::narf::make({ "native","array","of", "asci","string","literals" });

Or just make narf to contain a native char array:

auto buffer = dbj::narf::make("native char array");

Observe in the testing code how is default_print declared and implemented .  We use dbj::narf::apply, which  in turn calls dbj::narf::for_each , that uses std::for_each on the native arrys held inside the dbj::narf instance.

where begin() and end() are actually stl standards begin and end iterators to the native array as on the diagram above.  And we can do this quite easily by using the std::reference_wrapper, get() method that returns the (by now, famous) native array reference. Aka narf.

So. to get to the “holy grail” in a quite straightforward manner we just do:

// make ref to std array of 10 int's
auto arf = dbj::narf::make(std::array<int, 10>{});

// get to native array reference
const auto & native_arr_ref = arf.get()

And to get to the  pointer to the contained native array we simply do:

// default auto by-val result yields a pointer to the
// native array contained
auto native_arr_ptr = arf.get();

So if we wish to use some function that declares its argument as native array reference we will do it as simply as this:

template<typename T, size_t N>
void native_arr_consumer( const T(&arrf)[N] ) ;
// create narf
auto arf = dbj::narf::make({1,2,3});

native_arr_consumer(arf.get());

And of course we can always give the narf  instance to the range for loop like so:

// the for loop usage
// notice the auto & declaration
// so that internal array
// elements are updated
for (auto & element : arf.get()) {
element = random(255);
};

Please notice we do not need to know the size of the native array contained, still we enjoy the advantages of being able to use a reference to it.

In homage to std::data()

The,dbj::narf::data() is returning the reference of n-array it holds. Please note the difference to the std::data() version for native arrays, which returns a pointer to n-array.
// std version
template <class T, std::size_t N>
constexpr T* data(T(&array)[N]) noexcept{
   return array;
}

To use dbj::narf::data() the users will tend to use auto declaration of the return type , but one has to be a bit careful as the one will get different types in relation to the declaration of the result:

auto narf = dbj::narf::make("narf to Array of chars");
auto native_array_pointer = dbj::narf::data(narf);
decltype(auto) not_elegant_ref = dbj::narf::data(narf);
// the standard way
auto & the_arr_ref = dbj::narf::data(narf);

Danger Zone

is near when using dbj narf or just NARF’s. Please do not forget one can easily “drop the ball” and accidentally use dangling references in this case.

Keep in mind dbj::narf is just an alias to the

std::reference_wrapper<T[N]>

And in there the only data is T *.  Just one naked pointer.

Summary

In the coming days and weeks, I shall use dbj::narf extensively and report back with real-life usage examples and findings.

Stay tuned.