If you work with arrays of objects and offer some functions to execute on these arrays, it is common to pass an array pointer and the array size as function parameters. Arrays will often hold a huge amount of data. You normally don’t want to pass them as value and create a copy of the array. Therefore, if you pass an array as parameter it is transferred as pointer to the first element. This C++ feature is known as “array decay”. Once you convert an array into a pointer you lose the ability of the sizeof operator to count elements in the array. This lost ability is referred to as “decay” as the array decays into a pointer.
The following source code shows an example for array decaying. We have a class with a print function and we create an array of class instances. As we want to call the print function for all arrays, we create an according helper function. So, we pass the array and the array size as parameters to this function. As mentioned before, the array is passed as pointer to avoid copying the whole array. Therefore, the array is decayed into a pointer.
class MyClass { public: MyClass() : x(5) { } void Print() const { std::cout << x << std::endl; } private: int x; }; void PrintAll(const MyClass* elements, const int numberOfElements) { for (int index = 0; index < numberOfElements; ++index) { elements[index].Print(); } } int _tmain(int argc, _TCHAR* argv[]) { MyClass elements[10]; PrintAll(elements, 10); return 0; }
The example implementation works fine and prints the right results. So, array decaying seems to be a good and straight forward feature. But unfortunately, it is also a source of errors. As mentioned before, the decay comes together with a loss of size information. But size information is needed if we want to step through the array elements. The element size is calculated by the given array type, in this case “MyClass”. So, we have to think about the question what happens if the passed array is of a different type, for example of an object derived from “MyClass”. The following example shows an according implementation.
class MyClass { public: MyClass() : x(5) { } void Print() const { std::cout << x << std::endl; } private: int x; }; class MyDerivedClass : public MyClass { private: double y; }; void PrintAll(const MyClass* elements, const int numberOfElements) { for (int index = 0; index < numberOfElements; ++index) { elements[index].Print(); } } int _tmain(int argc, _TCHAR* argv[]) { MyDerivedClass elements[10]; PrintAll(elements, 10); return 0; }
If you execute this test application you will see strange outputs. This undefined behavior is a result of the array decay. The size information of the origin type “MyDerivedClass” gets lost. Within the function the size of “MyClass” is used to go to the next array position. Therefore, a wrong memory location is interpreted as class instance and so we end up in undefined behavior of the application.
The way that array names decay into pointers is fundamental to their use in C and C++. However, array decay interacts very badly with inheritance as this feature isn’t available in C. A logical guideline may be to use arrays in C only because array decay works fine in case there is no inheritance. In C++ we should instead use alternatives to arrays. So, you can use the build in vector type. The following source code shows the example application with change from array to vector.
class MyClass { public: MyClass() : x(5) { } void Print() const { std::cout << x << std::endl; } private: int x; }; class MyDerivedClass : public MyClass { private: double y; }; void PrintAll(std::vector<MyClass>& elements) { for (int index = 0; index < elements.size(); ++index) { elements[index].Print(); } } int _tmain(int argc, _TCHAR* argv[]) { std::vector<MyClass> elements(10); PrintAll(elements); // ok std::vector<MyDerivedClass> derivedElements(10); PrintAll(derivedElements); // compiler error, cannot convert vector<MyDerivedClass> to vector<MyClass> return 0; }
This time the implementation error can be detected by the compiler. The strong type safety system of C++ prevents the function call with an incompatible type. So we can pass a vector of “MyClass” instances but no vector of “MyDerivedClass” instances.
Summary
Array decay is the concept of passing an array as a pointer to the first element. As a side effect, the size information is lost. This will lead to undefined behavior if the passed array contains elements with a divergent size, for example like in most inheritance scenarios. Therefore it is recommended to use arrays in C code or in scenarios without inheritance only. In all other cases, you should use type safe collections like vectors.
Pingback: std::array vs c-style array | coders corner
Pingback: std::vector vs c-style array | coders corner