std::array and boost::array

When developing in C++, using C-style array is often a bad idea. Better using std::vector, the STL container that replaces C-style array offering features and capabilities of a STL container. On the other side, a std::vector is designed to be a dynamic container, this is cool, but sometimes there is no need for it, and it costs some overhead that we could avoid.

That's the point of using std::array, defined in C++0x, or boost::array, if your compiler does not support it yet. If we know the array size, and it is not supposed to change, we'll use a std::array; if we want to let a chance of dynamically change the size, we'll use a std::vector.

The code I'm showing hereafter is exctracted from a few testcases I have wrote using the google test framework. But even if you don't know what I'm talking about, it should be all quite easy to understand, at least in its general sense.

The code is written referring to the standard implementation but it is easy to switch to the boost container changing just the namespace and the include.

Initialization.

No constructor is provided for std::array, we just provide the referring type and its size:
std::array<int, 4> ai; // 1.
EXPECT_EQ(4, ai.size()); // 2.
std::for_each(ai.begin(), ai.end(), [](int i){ EXPECT_NE(0, i); }); // 3.

1. A std::array of four int has been put on the stack.
2. It contains four elements
3. We expect no initialization, so the chances that an element of the array would be set to zero should be pratically zero.

No construtctors, we said, but a std::array could be initialized using the assignment syntax used also for c-array, with the caution that we should use a double bracket, since the real array is a just member of the class we are initializing:
std::array<int, 4> a2 = {{ 42, 12, 9, 0 }}; // 1.
EXPECT_EQ(0, a2[3]); // 2.

1. Four ints in the array, initialized as specified.
2. The last element, index 3, is expected to have value zero.

Double brackets are a nuisance, and they do not add any information to the code, so the compiler could do without them. Besides, if we put less values between the brackets, default values are assumed:
std::array<int, 4> a3 = { 42, 12, 9 }; // 1.
EXPECT_EQ(4, a3.size()); // 2.
EXPECT_EQ(0, a3[3]); // 3.

1. No boring double brackets, and it works just the same.
2. The size is four, even if I specified just three values.
3. The last element has been initialized using the default value, that means zero.

We could even initialize a std::array with an empty pair of brackets:
std::array<int, 4> a4 = { };
std::for_each(a4.begin(), a4.end(), [](int i){ EXPECT_EQ(0, i); }); // 1.

1. An all zero element array is expected.

Assigning a value to all the elements.

We have just seen a practical way to initialize an std::array with all zero. But we need an alternative, for at least a couple of cases. Firstly, if our std::array is a member variable of a class we can't use that initializator. Secondly, we could need to set all the element of an array to a value different from zero. We can use indifferently two functions, that are actually synonym, assign() and fill():
std::array<int, 4> ai;
ai.assign(7); // 1.
std::for_each(ai.begin(), ai.end(), [](int i){ EXPECT_EQ(7, i); });

ai.fill(42); // 2.
std::for_each(ai.begin(), ai.end(), [](int i){ EXPECT_EQ(42, i); });

1. All the elements of the array are set to seven.
2. Same, but to fortytwo.

Swapping.

Given two arrays, same size, we could swap their content using a specific function:
std::array<int, 4> ai = { 42, 12, 9, 32 };
std::array<int, 4> a2 = { 24, 21, 7 };

EXPECT_EQ(42, ai[0]);
EXPECT_EQ(24, a2[0]);

a2.swap(ai);

EXPECT_EQ(24, ai[0]);
EXPECT_EQ(42, a2[0]);

Not much more to say on this function, I guess.

Getters and setters.

We can gain read and write access to a single element in a std::array in a number of way. Probably the most natural one is using the square brackets operator [], expecially if you have a background as a C programmer. It has the same limitations of its C counterpart: there is no check. It is just programmer responsibility avoid doing silly things like accessing elements outside the effective range.

The at() function works just like the [] operator, but it is not so trustful, and throws a std::out_of_range exception if it must.

And we have a couple of specialized functions to get the element at the beginning, front(), at at the end, back(), of the array:

std::array<int, 4> ai = { 42, 12, 9 };

EXPECT_EQ(0, ai.back()); // 1.
ai.back() = 33; // 2.
EXPECT_EQ(33, ai.back());

EXPECT_EQ(42, ai.front()); // 3.
ai.front() = 82;
EXPECT_EQ(82, ai.front());

EXPECT_EQ(12, ai.at(1));
EXPECT_THROW(ai.at(99), std::out_of_range); // 4.

1. Here we use back() to access the last array element to read it.
2. And here we actually change the value contained in the last element array.
3. front() works exactely as back(), as one would expect.
4. Our array does not have 99 elements, so trying to access that element would result in an exception.

Raw array.

When we need to acces the real stuff, usually because we should rely on some legacy code, we could extract the C-array in our std::array by calling the data() function:
std::array<int, 4> ai = { 42, 12, 9 };
int* ca = ai.data(); // 1.
for(size_t i = 0; i < ai.size(); ++i) // 2.
EXPECT_EQ(ca[i], ai[i]);

ca[3] = 99; // 3.
EXPECT_EQ(99, ai[3]);

1. ca now points to the C-array in our std::array.
2. We expect that we access the same stuff using both ca and ai.
3. And we expect that our changes made following the raw pointer result in changing the data as seen by std::array.

If we are using the Boost implementation, we also have access to another function, c_array(), that is merely a synonym of data():
boost::array<int, 4> ai = { 42, 12, 9 };

int* ca = ai.c_array();
for(size_t i = 0; i < ai.size(); ++i)
EXPECT_EQ(ca[i], ai[i]);

ca[3] = 99;
EXPECT_EQ(99, ai[3]);

Besides, the raw array is exposed as a public data member in the std::array (and in boost::array too), so we could be tempted to write the same code accessing it directly. Bad idea:
boost::array<int, 4> ai = { 42, 12, 9 };
ai.elems[3] = 99; // Boost - ???
//ai._Elems[3] = 99; // VC - ???
EXPECT_EQ(99, ai[3]);

Not a good a idea from an Object-Oriented point of view, since data should not be public if there is not a vary good reason that force you to to otherwise (here: having a way of initialize the raw array). Even though the data member is exposed, we should not access it directly, and here the reason is portability. Its name is not standardized, so any implementation could choose any fancy name.

Size.

Once created, a std::array won't change its size for all its life. Nevertheless we have a bunch of functions that are useful just from the point of view of homogeneity with the other STL containers:
std::array<int, 4> ai = { 42, 12, 9 };
EXPECT_FALSE(ai.empty()); // 1.
EXPECT_EQ(4, ai.size()); // 2.
EXPECT_EQ(ai.size(), ai.max_size()); // 3.

std::array<int, 0> a2; // 4.
EXPECT_TRUE(a2.empty());
EXPECT_EQ(0, a2.max_size());

1. empty() returns true if the array size is zero.
2. size() returns the actual size of the array.
3. max_size() is obviously the same of size().
4. Here is how to create a (pretty unuseful) zero size std::array.

No comments:

Post a Comment