Wednesday, May 5, 2010

struct

You've seen one subtle difference, between structs in C and structs in C++. There are quite a few very visible differences too. Let's have a look at one of them; constructors. A constructor is a well defined way of initialising an object, in this case an instance of a struct. When you create an instance of a struct in C, you define your variable, and then give the components their values. Not so in C++. You declare your struct with constructors, and define your struct instance with well known initial values. Here's how you can do it.
struct BoundsError {};
struct Range
{
Range(int upper_bound = 0, int lower_bound = 0)
throw (BoundsError);
// Precondition: upper_bound >= lower_bound
// Postconditions:
// lower == upper_bound
// upper == upper_bound
int lower;
int upper;
};
"BoundsError" is a struct with no data, and it's used entirely for error checking. For this example, no data is needed for it. The struct "Range" is known to have two components "lower" and "upper" (usually referred to as member variables) and a constructor. You recognise a constructor as something that looks like a function prototype, declared inside the struct, and with the same name as the struct. Since C++ allows function overloading on parameter types, it is possible to specify multiple different constructors. Here you see yet something new; default parameter values. C++ allows functions to have default parameter values, and these values are used if you don't provide any when calling the function. In this case, it appears as if three constructors were called, one with no parameters, initialising both "upper" and "lower" to 0, one with one parameter, initialising "lower" to 0, and one with two parameters. The restriction on default parameter values is that you can only add them from the right on the parameter list. Here's a few examples for you:
void print(const char*p = "default value")
{
cout << p << endl;
}
void print(int a, int b=3)
{
cout << a << "," << b << endl;
}
void print(unsigned a=0, int b) // ERROR!!! Default
{
cout << a << "," << b << endl; // parameters
from the } // right only.
print(); // prints "default value");
print("something"); // calls print(const char*);
print(5); // prints 5,3
print(5,5); // prints 5,5
So far we have just said that the constructor exists, not what it does. Here comes that part:
Range::Range(int upper_bound, int lower_bound)
throw (BoundsError)
: lower(lower_bound), //*1
upper(upper_bound) //*1
{
// Preconditions.
if (upper_bound < lower_bound) throw BoundsError(); //*2
// Postconditions.
if (lower != lower_bound) throw BoundsError();
if (upper != upper_bound) throw BoundsError();
}
Quite a handful of new things for so few lines. Let's break them apart in three pieces:
Range::Range(int upper_bound, int lower_bound)
throw (BoundsError)
This is the constructor declarator. The first "Range" says we're dealing with the struct called "Range". The "::" is the scope operator (the same as in //**9** in the example for variable scope in the beginning in this article), means we're defining something that belongs to the struct. The rest of this line is the same as you saw in the declaration, except that the default parameter values are not listed here; they're an interface issue only. This might seem like redundant information, but it is not. If you forget "::", what you have is a function called "Range(int, int)" that returns a "Range". If you forget any of the "Range" you have a syntax error.
Now to the second piece, the one with //*1 comments.
In a constructor you can add what's called an initialiser list between the declarator and the function body. The initialiser list is a comma separated list of member variables that you give an initial value. This can of course be done in the function body as well, but if you can give the member variables a value in the initialiser list, you should. The reason is that the member variables will be initialised whether you specify it in a list it or not, and if you initialise them in the function body only, they will in fact be initialised twice. One thing that is important to remember with initialiser lists is that the order of initialisation is the order the member variables appear in the struct declaration. Do not try to change the order by reorganising the initialiser list because it will not work. [Some compilers will just rearrange the order of the list internally to be the right one and tell you they've done so; but don't rely on this -- Ed]
Last is the function body:
{
// Preconditions.
if (upper_bound < lower_bound) throw BoundsError(); //*2
// Postconditions.
if (lower != lower_bound) throw BoundsError();
if (upper != upper_bound) throw BoundsError();
}
This looks just like a normal function body. Member variables that we for some reason have been unable to initialise in the initialiser list can be taken care of here. In this case all were initialised before entering the function body, so nothing such is needed. Instead we check that the range is valid, and that the components were initialised as intended, and throw an exception if it isn't. "BoundsError()" at //*2, means a nameless instance of struct "BoundsError". Note that even if you define a constructor, for which the function body is empty, it's still needed

No comments:

Post a Comment