Quantcast
Channel: Everyday I'm coding » pod
Viewing all articles
Browse latest Browse all 2

What are Aggregates and PODs and how/why are they special?

0
0

This FAQ is about Aggregates and PODs and covers the following material:

  • What are Aggregates?
  • What are PODs (Plain Old Data)?
  • How are they related?
  • How and why are they special?
  • What changes for C++11?


Solution:

How to read:

This article is rather long. If you want to know about both aggregates and POD’s (Plain Old Data) take time and read it. If you are interested just in aggregates, read only the first part. If you are interested only in POD’s then you must first read the definition, implications, and examples of aggregates and then you may jump to POD’s but I would still recommend reading the first part in its entirety. The notion of aggregates is essential for defining POD’s. If you find any errors (even minor, including grammar, stylistics, formatting, syntax, etc.) please leave a comment, I’ll edit.

What are aggregates and why they are special

Formal definition from the C++ standard (C++03 8.5.1 §1):

An aggregate is an array or a class (clause 9) with no user-declared
constructors (12.1), no private or protected non-static data members (clause 11),
no base classes (clause 10), and no virtual functions (10.3).

So, OK, let’s parse this definition. First of all, any array is an aggregate. A class can also be an aggrgate if… wait! nothing is said about structs or unions, can’t they be aggregates? Yes, they can. In C++ the term class refers to all classes, structs, and unions. So, a class(struct, union) is an aggregate if and only if it satisfies the criteria from the above definitions. What do these criteria imply?

  • This does not mean an aggregate class cannot have constructors, in fact it can have a default constructor and/or a copy constructor as long as they are implicitly declared by the compiler, and not explicitly by the user

  • No private or protected non-static data members. You can have as many private and protected member functions (but not constructors) as well as as many private or protected static data members and member functions as you like and not violate the rules for aggregate classes

  • An aggregate class can have a user-declared/user-defined copy-assignment operator and/or destructor

  • An array is an aggregate even it is an array of non-aggregate class type.

Now it’s time to take a look at some examples:

class NotAggregate1
{
 virtual void f(){} //remember? no virtual functions
};

class NotAggregate2
{
 int x; //x is private by default and non-static 
};

class NotAggregate3
{
public:
    NotAggregate3(int) {} //oops, user-defined constructor
};

class Aggregate1
{
public:
    NotAggregate1 member1;   //ok, public member
    Aggregate1& operator = (Aggregate1 const & rhs) {/* */} //ok, copy-assignment  
private:
   void f() {} // ok, just a private function

};

You get the idea. Now it’s time to see how aggregates are special. They, unlike non-aggregate classes, can be initialized with curly braces {}. This initialization syntax is commonly known for arrays, and we just learnt that these are aggregates. So, let’s start with them.

Type array_name[n] = {a1, a2, …, am};

if(m == n)
the ith element of the array is initialized with ai
else if(m < n)
the first m elements of the array are initialized with a1, a2, …, am and the other n - m elements are, if possible, value-initialized (see below for the explanation of the term)
else if(m > n)
the compiler will issue an error
else (this is the case when n isn’t specified at all like int a[] = {1,2,3};)
the size of the array (n) is assumed to be equal to m, so int a[] = {1,2,3} is equivalent to int a[3] = {1,2,3};

When an object of scalar type (bool, int, char, double, pointers etc.) is value-initialized it means it is initialized with 0 (false for bool, 0.0 for double etc.). When an object of class type with a user-declared default constructor is value-initialized its default constructor is called. If the default constructor is implicitly defined then all nonstatic members are recursively value-initialized. This definition is imprecise and a bit incorrect but it should give you the basic idea. I may write another faq on differences between zero-initialization, value-initialization and default-initialization in the near future. A reference cannot be value-initialized. Value-initialization for a non-aggregate class can fail if, for example, the class has no appropriate default constructor.

Examples of array initialization:

class A()
{
   A(int){} //no default constructor
};
class B()
{
   B() {} //default constructor available
};
int main()
{
  A a1[3] = {A(2), A(1), A(14)}; //OK n == m
  A a2[3] = {A(2)}; //ERROR A has no default constructor. Unable to value-initialize a2[1] and a2[2]
  B b1[3] = {B()}; //OK b1[1] and b1[2] are value initialized, in this case with the default-ctor
  int Array1[1000] = {0}; //All elements are initialized with 0;
  int Array2[1000] = {1}; //Attention: only the first element is 1, the rest are 0;
  bool Array3[1000] = {}; //the braces can be empty too. All elements initialized with false
  int Array4[1000]; //no initializer. This is different from an empty {} initializer in that
  //the elements in this case are not value-initialized, but have indeterminate values 
  //(unless, of course, Array4 is a global array)
  int array[2] = {1,2,3,4}; //ERROR, too many initializers

Now let’s see how aggregate classes can be initialized with braces. Pretty much the same way. Instead of the array elements we will initialize the non-static data members in the order of their appearance in the class definition (they are all public by definition). If there are fewer initializers than members, the rest are value-initialized. If it is impossible to value-initialize one of the members which were not explicitly initialized, we get a compile-time error. If there are more initializers than necessary, we get a compile-time error as well.

struct X{
    int i1;
 int i2;
};
struct Y{
    char c;
 X x;
 int i[2];
    float f; 
protected:
 static double d;
private:
    void g(){}      
}; 

Y y = {'a', {10,20}, {20,30}};

In the above example y.c is initialized with ‘a’, y.x.i1 with 10, y.x.i2 with 20, y.i[0] with 20, y.i[1] with 30 and y.f is value-initialized, that is, initialized with 0.0. The protected static member d is not initialized at all, because it is static.

Aggregate unions are different in that you may initialize only their first member with braces. I think that if you are advanced enough in C++ to even consider using unions (their use may be very dangerous and must be thought of carefully), you could look up the rules for unions in the standard yourself :) .

Now that we know what’s special about aggregates let’s try to understand the restrictions on classes, that is, why they are there. We should understand that memberwise initialization with braces implies that the class is nothing more than the sum of its members. If a user-defined constructor is present, it means that the user needs to do some extra work to initialize the members therefore brace initialization would be incorrect. If virtual functions are present, it means that the class has (on most implementations) a pointer to the so-called vtable of the class, which is set in the constructor, so brace-initialization would be insufficient. You could figure out the rest of the restrictions in a similar manner as an exercise :)

So much about the aggregates. Now we can define a stricter set of types, to wit, POD’s

What are POD’s and why they are special

Formal definition from the C++ standard (C++03 9 §4):

A POD-struct is an aggregate class
that has no non-static data members of
type non-POD-struct, non-POD-union (or
array of such types) or reference, and
has no user-defined copy assignment
operator and no user-defined
destructor. Similarly, a POD-union is
an aggregate union that has no
non-static data members of type
non-POD-struct, non-POD-union (or
array of such types) or reference, and
has no user-defined copy assignment
operator and no user-defined
destructor. A POD class is a class
that is either a POD-struct or a
POD-union.

Wow, this one’s tougher to parse, isn’t it? :) Let’s leave unions out (on the same grounds as above) and rephrase in a bit clearer way

An aggregate class is called a POD if
it has no user-defined copy-assignment
operator and destructor and none of
its nonstatic members is a non-POD
class, array of non-POD, or a
reference.

What does this definition imply? (did I mention POD stands for Plain Old Data ?:)

  • All POD classes are aggregates, or, to put it the other way around, if a class is not an aggregate then it is sure not a POD
  • classes, just like structs can be PODs even though the standard term is POD-struct for both cases
  • just like in case of aggregates, it doesn’t matter what static members the class has

Examples:

struct POD
{
 int x;
 char y;
 void f() {} //no harm if there's a function
 static std::vector<char> v; //static members do not matter
};

struct AggregateButNotPOD1
{
 int x;
 ~AggregateButNotPOD1(){} //user-defined destructor
};

struct AggregateButNotPOD2
{
 AggregateButNotPOD1 arrOfNonPod[3]; //array of non-POD class
};

POD-classes, POD-unions, scalar types, and arrays of such types are collectively called POD-types.
POD’s are special in an extremely many ways. I’ll provide just some examples.

  • POD-classes are the closest to C structs. Unlike them, POD’s can have member functions and arbitrary static members, but neither of these two changes the memory layout of the object. So if you want to write a more or less portable dynamic library which can be used from C and even .NET you should try to make all your exported functions take and return only parameters of POD-types

  • The lifetime of objects of non-POD class type begins when the constructor has finished and ends when the destructor has finished. For POD classes, the lifetime begins when storage for the object is occupied and finishes when that storage is released or reused.

  • For objects of POD types it is guaranteed by the standard that when you memcpy the contents of your object into an array of char or unsigned char, and then memcpy the contents back into your object, the object will hold its original value. Do note that there is no such guarantee for objects non-POD types. Also, you can safely copy POD objects with memcpy. The following example assumes T is a POD-type

    #define N sizeof(T)
    char buf[N];
    T obj; // obj initialized to its original value
    memcpy(buf, &obj, N); // between these two calls to memcpy,
    // obj might be modified
    memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
    // holds its original value
    
  • goto statement. As you may know, it is illegal (the compiler should issue an error) to make a jump via goto from a point where some variable was not yet in scope to a point where it is already in scope. This restriction applies only if the variable is of non-POD type. In the following example f() is ill-formed whereas g() is well-formed. Note that Microsoft compiler is too liberal with this rule – just issues a warning in both cases.

    int f() {
      struct NonPOD { NonPOD(){}};
      goto label;
      NonPOD x;
    label:
      return 0;
    }
    
    int g(){
      struct POD {int i;  char c;};
      goto label;
      POD x;
    label:
      return 0;
    }
    
  • It is guaranteed that there will be no padding in the beginning of a POD object. In other words if a POD-class A’s first member is of type T you can safely reinterpret_cast from A* to T* and get the pointer to the first member and vice versa.

The list goes on and on…

Conclusion

It is important to understand what exactly a POD is because many language features, as you see, behave differently for them. I hope this was interesting and useful to read.


Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images