Previous Lecture | Next Lecture | Exercises | Top Level

Typing and Access Control

Contents

Now that you have had some programming experience, you will have noticed that correcting compiler errors is a lot easier than finding run-time errors.

Good programming languages therefore provide tools to make the compiler smarter and enable it to find logical mistakes in your program.

This is achieved through typing and access control. We not only define the structure of the data and how we manipulate it, but we also define the meaning of the data and who is allowed to do what with it.


Next Section | Contents

Content Dependent Access Control

"You never compare apples and oranges". But the following grocery store program will compile and run without problems, even though it doesn't do what the programmer intended:

int price_of_apples(int apples) { return 120*apples; // 120 Yen per apple - cheap eh? } main() { int oranges; oranges = 3; price_of_apples(oranges); }

Now, if we could convey to the compiler the information that apples and oranges are two different things, even though both are represented by an integer, then we could detect this error at compile time.

By using classes, the compiler will tell us that we are doing something strange:

class Oranges {public: int amount; }; class Apples {public: int amount; }; int price_of_apples(Apples apples) { return 120*apples.amount; } main() { Oranges oranges = {3}; price_of_apples(oranges); } The compiler will whine about a missing conversion function. If we wanted to, we could describe a conversion function from apples to oranges like this: class Oranges { public: // For the price of one apple, you can get 2 oranges Oranges(Apples apples) { amount = 2*apples.amount; } int amount; }; class Apples { public: Apples(Oranges oranges) { amount = oranges.amount/2; } int amount; }; int price_of_apples(Apples apples) { return 120*apples.amount; } main() { Oranges oranges = {3}; price_of_apples(oranges); } Now the program would compile, and the meaning would be something like "How many apples could I buy for the price of 3 oranges?".

Using a constructor this way is called defining a conversion between apples and oranges.

In C++, many conversions are definied implicitly. All base types (int, float, double, char) convert freely, although some might generate a warning.

Implicit conversion is both good and bad. It's good because it saves us a lot of typing, and makes "obvious" equivalences work. It's bad because we lose the effect of using the same data representation for distinct purposes (which is why we couldn't use typedef in the example above).

When using classes, conversions have to be defined explicitly. These conversions document the relationship between classes. If no conversion is defined, we get a compiler error.

Another way to express relationships between classes is via inheritance. The following would be a more interesting way to code out grocery store program:

class Fruit { public: virtual int unit_price() {return -1; // unknown}; int amount; }; class Apple: public Fruit { public: virtual int unit_price() { return 120; } }; class Orange: public Fruit { public: virtual int unit_price() { return 60; } }; int price(Fruit fruit) { return fruit.amount * fruit.price(); } main() { Oranges oranges; // ... code ... price(oranges); // ... more code ... } There is a trivial conversion between a class and it's ancestors. Since every derivation adds stuff to a class, we simply ignore the excess and just use whatever the base class knows about.

This conversion is not possible the other way. We would have to guess what features the child will have. The only way for parent classes to communicate with their children is via virtual functions, like in the example.

One way to force a conversion is to use a cast. Here is a simple example of using a cast:

double round(double number) { return (int)(number + 0.5); } This function rounds a floating point number to the nearest integer. The cast forces a conversion from floating point to integer. That conversion simply chops off the fractional part, which is why we add 0.5 to achieve the correct rounding to the nearest integer. Since the return type is again double, we get an automatic conversion of the integer back into a floating point number.


Next Section | Previous Section

User Dependent Access Control

Another way to prevent accidental or negligent abuse of classes is by controlling the use of class components, or, more exactly, who gets to use which class component.

C++ knows three kinds of protections:

public:
Everybody may access and use public components.

private:
Nobody but the member functions of the class itself are permitted to use private components.

protected:
Member functions of the class itself and it's descendents may use protected components.


Contents | Previous Section

Method Dependent Access Control

One of the most important acces control features is read-protecting object components. This is done with the const keyword.

There are actually three usages of const:

Click here to see a program with all three cases in action.
Previous Lecture | Next Lecture | Exercises | Contents
Christian Goetze