Table of Contents Previous Next
Logo
The Slice Language : 4.11 Classes
Copyright © 2003-2008 ZeroC, Inc.

4.11 Classes

In addition to interfaces, Slice permits the definition of classes. Classes are like interfaces in that they can have operations and are like structures in that they can have data members. This leads to hybrid objects that can be treated as interfaces and passed by reference, or can be treated as values and passed by value. Classes provide much architectural flexibility. For example, classes allow behavior to be implemented on the client side, whereas interfaces allow behavior to be implemented only on the server side.
Classes support inheritance and are therefore polymorphic: at run time, you can pass a class instance to an operation as long as the actual class type is derived from the formal parameter type in the operation’s signature. This also permits classes to be used as type-safe unions, similarly to Pascal’s discriminated variant records.

4.11.1 Simple Classes

A Slice class definition is similar to a structure definition, but uses the class keyword. For example:
class TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};
Apart from the keyword class, this definition is identical to the structure definition we saw on page 95. You can use a Slice class wherever you can use a Slice structure (but, as we will see shortly, for performance reasons, you should not use a class where a structure is sufficient). Unlike structures, classes can be empty:
class EmptyClass {};    // OK
struct EmptyStruct {};  // Error
Much the same design considerations as for empty interfaces (see page 122) apply to empty classes: you should at least stop and rethink your approach before committing yourself to an empty class.

4.11.2 Class Inheritance

Unlike structures, classes support inheritance. For example:
class TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};

class DateTime extends TimeOfDay {
    short day;          // 1  31
    short month;        // 1  12
    short year;         // 1753 onwards
};
This example illustrates one major reason for using a class: a class can be extended by inheritance, whereas a structure is not extensible. The previous example defines DateTime to extend the TimeOfDay class with a date.1
Classes only support single inheritance. The following is illegal:
class TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};

class Date {
    short day;
    short month;
    short year;
};

class DateTime extends TimeOfDay, Date {   // Error!
    // ...
};
A derived class also cannot redefine a data member of its base class:
class Base {
    int integer;
};

class Derived extends Base {
    int integer;                // Error, integer redefined
};

4.11.3 Class Inheritance Semantics

Classes use the same pass-by-value semantics as structures. If you pass a class instance to an operation, the class and all its members are passed. The usual type compatibility rules apply: you can pass a derived instance where a base instance is expected. If the receiver has static type knowledge of the actual derived run-time type, it receives the derived instance; otherwise, if the receiver does not have static type knowledge of the derived type, the instance is sliced to the base type. For an example, suppose we have the following definitions:
// In file Clock.ice:

class TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};

interface Clock {
    TimeOfDay getTime();
    void setTime(TimeOfDay time);
};


// In file DateTime.ice:

#include <Clock.ice>

class DateTime extends TimeOfDay {
    short day;          // 1  31
    short month;        // 1  12
    short year;         // 1753 onwards
};
Because DateTime is a sub-class of TimeOfDay, the server can return a DateTime instance from getTime, and the client can pass a DateTime instance to setTime. In this case, if both client and server are linked to include the code generated for both Clock.ice and DateTime.ice, they each receive the actual derived DateTime instance, that is, the actual run-time type of the instance is preserved.
Contrast this with the case where the server is linked to include the code generated for both Clock.ice and DateTime.ice, but the client is linked only with the code generated for Clock.ice. In other words, the server understands the type DateTime and can return a DateTime instance from getTime, but the client only understands TimeOfDay. In this case, the derived DateTime instance returned by the server is sliced to its TimeOfDay base type in the client. (The information in the derived part of the instance is simply lost to the client.)
Class hierarchies are useful if you need polymorphic values (instead of polymorphic interfaces). For example:
class Shape {
    // Definitions for shapes, such as size, center, etc.
};

class Circle extends Shape {
    // Definitions for circles, such as radius...
};

class Rectangle extends Shape {
    // Definitions for rectangles, such as width and length...
};

sequence<Shape> ShapeSeq;

interface ShapeProcessor {
    void processShapes(ShapeSeq ss);
};
Note the definition of ShapeSeq and its use as a parameter to the processShapes operation: the class hierarchy allows us to pass a polymorphic sequence of shapes (instead of having to define a separate operation for each type of shape).
The receiver of a ShapeSeq can iterate over the elements of the sequence and down-cast each element to its actual run-time type. (The receiver can also ask each element for its type ID to determine its type—see Section 6.14.1 and Section 10.11.2.)

4.11.4 Classes as Unions

Slice does not offer a dedicated union construct because it is redundant. By deriving classes from a common base class, you can create the same effect as with a union:
interface ShapeShifter {
    Shape translate(Shape s, long xDistance, long yDistance);
};
The parameter s of the translate operation can be viewed as a union of two members: a Circle and a Rectangle. The receiver of a Shape instance can use the type ID (see Section 4.13) of the instance to decide whether it received a Circle or a Rectangle. Alternatively, if you want something more along the lines of a conventional discriminated union, you can use the following approach:
class UnionDiscriminator {
    int d;
};

class Member1 extends UnionDiscriminator {
    // d == 1
    string s;
    float f;
};

class Member2 extends UnionDiscriminator {
    // d == 2
    byte b;
    int i;
};
With this approach, the UnionDiscriminator class provides a discriminator value. The "members" of the union are the classes that are derived from UnionDiscriminator. For each derived class, the discriminator takes on a distinct value. The receiver of such a union uses the discriminator value in a switch statement to select the active union member.

4.11.5 Self-Referential Classes

Classes can be self-referential. For example:
class Link {
    SomeType value;
    Link next;
};
This looks very similar to the self-referential interface example on page 122, but the semantics are very different. Note that value and next are data members, not operations, and that the type of next is Link (not Link*). As you would expect, this forms the same linked list arrangement as the Link interface on page 122: each instance of a Link class contains a next member that points at the next link in the chain; the final link’s next member contains a null value. So, what looks like a class including itself really expresses pointer semantics: the next data member contains a pointer to the next link in the chain.
You may be wondering at this point what the difference is then between the Link interface on page 122 and the Link class on page 131. The difference is that classes have value semantics, whereas proxies have reference semantics. To illustrate this, consider the Link interface from page 122 once more:
interface Link {
    idempotent SomeType getValue();
    idempotent Link*    next();
};
Here, getValue and next are both operations and the return value of next is Link*, that is, next returns a proxy. A proxy has reference semantics, that is, it denotes an object somewhere. If you invoke the getValue operation on a Link proxy, a message is sent to the (possibly remote) servant for that proxy. In other words, for proxies, the object stays put in its server process and we access the state of the object via remote procedure calls. Compare this with the definition of our Link class:
class Link {
    SomeType value;
    Link next;
};
Here, value and next are data members and the type of next is Link, which has value semantics. In particular, while next looks and feels like a pointer, it cannot denote an instance in a different address space. This means that if we have a chain of Link instances, all of the instances are in our local address space and, when we read or write a value data member, we are performing local address space operations. This means that an operation that returns a Link instance, such as getHead, does not just return the head of the chain, but the entire chain, as shown in Figure 4.8.
Figure 4.8. Class version of Link before and after calling getHead.
On the other hand, for the interface version of Link, we do not know where all the links are physically implemented. For example, a chain of four links could have each object instance in its own physical server process; those server processes could be each in a different continent. If you have a proxy to the head of this four-link chain and traverse the chain by invoking the next operation on each link, you will be sending four remote procedure calls, one to each object
Self-referential classes are particularly useful to model graphs. For example, we can create a simple expression tree along the following lines:
enum UnaryOp { UnaryPlus, UnaryMinus, Not };
enum BinaryOp { Plus, Minus, Multiply, Divide, And, Or };

class Node {};

class UnaryOperator extends Node {
    UnaryOp operator;
    Node operand;
};

class BinaryOperator extends Node {
    BinaryOp op;
    Node operand1;
    Node operand2;
};

class Operand extends Node {
    long val;
};
The expression tree consists of leaf nodes of type Operand, and interior nodes of type UnaryOperator and BinaryOperator, with one or two descendants, respectively. All three of these classes are derived from a common base class Node. Note that Node is an empty class. This is one of the few cases where an empty base class is justified. (See the discussion on page 122; once we add operations to this class hierarchy (see Section 4.11.7), the base class is no longer empty.)
If we write an operation that, for example, accepts a Node parameter, passing that parameter results in transmission of the entire tree to the server:
interface Evaluator {
    long eval(Node expression); // Send entire tree for evaluation
};
Self-referential classes are not limited to acyclic graphs; the Ice run time permits loops: it ensures that no resources are leaked and that infinite loops are avoided during marshaling.

4.11.6 Classes Versus Structures

One obvious question to ask is: why does Ice provide structures as well as classes, when classes obviously can be used to model structures? The answer has to do with the cost of implementation: classes provide a number of features that are absent for structures:
• Classes support inheritance.
• Classes can be self-referential.
• Classes can have operations (see Section 4.11.7).
• Classes can implement interfaces (see Section 4.11.9).
Obviously, an implementation cost is associated with the additional features of classes, both in terms of the size of the generated code and the amount of memory and CPU cycles consumed at run time. On the other hand, structures are simple collections of values ("plain old structs") and are implemented using very efficient mechanisms. This means that, if you use structures, you can expect better performance and smaller memory footprint than if you would use classes (especially for languages with direct support for "plain old structures", such as C++ and C#). Use a class only if you need at least one of its more powerful features.

4.11.7 Classes with Operations

Classes, in addition to data members, can have operations. The syntax for operation definitions in classes is identical to the syntax for operations in interfaces. For example, we can modify the expression tree from Section 4.11.5 as follows:
enum UnaryOp { UnaryPlus, UnaryMinus, Not };
enum BinaryOp { Plus, Minus, Multiply, Divide, And, Or };

class Node {
    idempotent long eval();
};

class UnaryOperator extends Node {
    UnaryOp operator;
    Node operand;
};

class BinaryOperator extends Node {
    BinaryOp op;
    Node operand1;
    Node operand2;
};

class Operand {
    long val;
};
The only change compared to the version in Section 4.11.5 is that the Node class now has an eval operation. The semantics of this are as for a virtual member function in C++: each derived class inherits the operation from its base class and can choose to override the operation’s definition. For our expression tree, the Operand class provides an implementation that simply returns the value of its val member, and the UnaryOperator and BinaryOperator classes provide implementations that compute the value of their respective subtrees. If we call eval on the root node of an expression tree, it returns the value of that tree, regardless of whether we have a complex expression or a tree that consists of only a single Operand node.
Operations on classes are normally executed in the caller’s address space, that is, operations on classes are local operations that do not result in a remote procedure call.2 Of course, this immediately raises an interesting question: what happens if a client receives a class instance with operations from a server, but client and server are implemented in different languages? Classes with operations require the receiver to supply a factory for instances of the class. The Ice run time only marshals the data members of the class. If a class has operations, the receiver of the class must provide a class factory that can instantiate the class in the receiver’s address space, and the receiver is responsible for providing an implementation of the class’s operations.
Therefore, if you use classes with operations, it is understood that client and server each have access to an implementation of the class’s operations. No code is shipped over wire (which, in an environment of heterogeneous nodes using different operating systems and languages is infeasible).

4.11.8 Architectural Implications of Classes

Classes have a number of architectural implications that are worth exploring in some detail.

Classes without Operations

Classes that do not use inheritance and only have data members (whether self-referential or not) pose no architectural problems: they simply are values that are marshaled like any other value, such as a sequence, structure, or dictionary. Classes using derivation also pose no problems: if the receiver of a derived instance has knowledge of the derived type, it simply receives the derived type; otherwise, the instance is sliced to the most-derived type that is understood by the receiver. This makes class inheritance useful as a system is extended over time: you can create derived class without having to upgrade all parts of the system at once.

Classes with Operations

Classes with operations require additional thought. Here is an example: suppose that you are creating an Ice application. Also assume that the Slice definitions use quite a few classes with operations. You sell your clients and servers (both written in Java) and end up with thousands of deployed systems.
As time passes and requirements change, you notice a demand for clients written in C++. For commercial reasons, you would like to leave the development of C++ clients to customers or a third party but, at this point, you discover a glitch: your application has lots of classes with operations along the following lines:
class ComplexThingForExpertsOnly {
    // Lots of arcane data members here...
    MysteriousThing mysteriousOperation(/* parameters */);
    ArcaneThing arcaneOperation(/* parameters */);
    ComplexThing complexOperation(/* parameters */);
    // etc...
};
It does not matter what exactly these operations do. (Presumably, you decided to off-load some of the processing for your application onto the client side for performance reasons.) Now that you would like other developers to write C++ clients, it turns out that your application will work only if these developers provide implementations of all the client-side operations and, moreover, if the semantics of these operations exactly match the semantics of your Java implementations. Depending on what these operations do, providing exact semantic equivalents in a different language may not be trivial, so you decide to supply the C++ implementations yourself. But now, you discover another problem: the C++ clients need to be supported for a variety of operating systems that use a variety of different C++ compilers. Suddenly, your task has become quite daunting: you really need to supply implementations for all the combinations of operating systems and compiler versions that are used by clients. Given the different state of compliance with the ISO C++ standard of the various compilers, and the idiosyncrasies of different operating systems, you may find yourself facing a development task that is much larger than anticipated. And, of course, the same scenario will arise again should you need client implementations in yet another language.
The moral of this story is not that classes with operations should be avoided; they can provide significant performance gains and are not necessarily bad. But, keep in mind that, once you use classes with operations, you are, in effect, using client-side native code and, therefore, you can no longer enjoy the implementation transparencies that are provided by interfaces. This means that classes with operations should be used only if you can tightly control the deployment environment of clients. If not, you are better off using interfaces and classes without operations. That way, all the processing stays on the server and the contract between client and server is provided solely by the Slice definitions, not by the semantics of the additional client-side code that is required for classes with operations.

Classes for Persistence

Ice also provides a built‑in persistence mechanism that allows you to store the state of a class in a database with very little implementation effort. To get access to these persistence features, you must define a Slice class whose members store the state of the class. We discuss the persistence features of Slice in detail in Chapter 36.

4.11.9 Classes Implementing Interfaces

A Slice class can also be used as a servant in a server, that is, an instance of a class can be used to provide the behavior for an interface, for example:
interface Time {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time);
};

class Clock implements Time {
    TimeOfDay time;
};
The implements keyword indicates that the class Clock provides an implementation of the Time interface. The class can provide data members and operations of its own; in the preceding example, the Clock class stores the current time that is accessed via the Time interface. A class can implement several interfaces, for example:
interface Time {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time);
};

interface Radio {
    idempotent void setFrequency(long hertz);
    idempotent void setVolume(long dB);
};

class RadioClock implements Time, Radio {
    TimeOfDay time;
    long hertz;
};
The class RadioClock implements both Time and Radio interfaces.
A class, in addition to implementing an interface, can also extend another class:
interface Time {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time);
};

class Clock implements Time {
    TimeOfDay time;
};

interface AlarmClock extends Time {
    idempotent TimeOfDay getAlarmTime();
    idempotent void setAlarmTime(TimeOfDay alarmTime);
};

interface Radio {
    idempotent void setFrequency(long hertz);
    idempotent void setVolume(long dB);
};

class RadioAlarmClock extends Clock
                      implements AlarmClock, Radio {
    TimeOfDay alarmTime;
    long hertz;
};
These definitions result in the inheritance graph shown in Figure 4.9:
Figure 4.9. A Class using implementation and interface inheritance.
For this definition, Radio and AlarmClock are abstract interfaces, and Clock and RadioAlarmClock are concrete classes. As for Java, a class can implement multiple interfaces, but can extend at most one class.

4.11.10 Class Inheritance Limitations

As for interface inheritance, a class cannot redefine an operation or data member that it inherits from a base interface or class. For example:
interface BaseInterface {
    void op();
};

class BaseClass {
    int member;
};

class DerivedClass extends BaseClass implements BaseInterface {
    void someOperation();       // OK
    int op();                   // Error!
    int  someMember;            // OK
    long member;                // Error!
};

4.11.11 Pass-by-Value Versus Pass-by-Reference

As we saw in Section 4.11.5, classes naturally support pass-by-value semantics: passing a class transmits the data members of the class to the receiver. Any changes made to these data members by the receiver affect only the receiver’s copy of the class; the data members of the sender’s class are not affected by the changes made by the receiver.
In addition to passing a class by value, you can pass a class by reference. For example:
class TimeOfDay {
    short hour;
    short minute;
    short second;
    string format();
};

interface Example {
     TimeOfDay* get();  // Note: returns a proxy!
};
Note that the get operation returns a proxy to a TimeOfDay class and not a TimeOfDay instance itself. The semantics of this are as follows:
• When the client receives a TimeOfDay proxy from the get call, it holds a proxy that differs in no way from an ordinary proxy for an interface.
• The client can invoke operations via the proxy, but cannot access the data members. This is because proxies do not have the concept of data members, but represent interfaces: even though the TimeOfDay class has data members, only its operations can be accessed via a the proxy.
The net effect is that, in the preceding example, the server holds an instance of the TimeOfDay class. A proxy for that instance was passed to the client. The only thing the client can do with this proxy is to invoke the format operation. The implementation of that operation is provided by the server and, when the client invokes format, it sends an RPC message to the server just as it does when it invokes an operation on an interface. The implementation of the format operation is entirely up to the server. (Presumably, the server will use the data members of the TimeOfDay instance it holds to return a string containing the time to the client.)
The preceding example looks somewhat contrived for classes only. However, it makes perfect sense if classes implement interfaces: parts of your application can exchange class instances (and, therefore, state) by value, whereas other parts of the system can treat these instances as remote interfaces. For example:
interface Time {
    string format();
    // ...
};

class TimeOfDay implements Time {
    short hour;
    short minute;
    short second;
};

interface I1 {
     TimeOfDay get();           // Pass by value
     void put(TimeOfDay time);  // Pass by value
};

interface I2 {
    Time* get();                // Pass by reference
};
In this example, clients dealing with interface I1 are aware of the TimeOfDay class and pass it by value whereas clients dealing with interface I2 deal only with the Time interface. However, the actual implementation of the Time interface in the server uses TimeOfDay instances.
Be careful when designing systems that use such mixed pass-by-value and pass-by-reference semantics. Unless you are clear about what parts of the system deal with the interface (pass by reference) aspects and the class (pass by value) aspects, you can end up with something that is more confusing than helpful.
A good example of putting this feature to use can be found in Freeze (see Chapter 36), which allows you to add classes to an existing interface to implement persistence.

4.11.12 Passing Interfaces by Value

Consider the following definitions:
interface Time {
    idempotent TimeOfDay getTime();
    // ...
};

interface Record {
    void addTimeStamp(Time t); // Note: Time t, not Time* t
    // ...
};
Note that addTimeStamp accepts a parameter of type Time, not of type Time*. The question is, what does it mean to pass an interface by value? Obviously, at run time, we cannot pass an an actual interface to this operation because interfaces are abstract and cannot be instantiated. Neither can we pass a proxy to a Time object to addTimeStamp because a proxy cannot be passed where an interface is expected.
However, what we can pass to addTimeStamp is something that is not abstract and derives from the Time interface. For example, at run time, we could pass an instance of our TimeOfDay class from the previous section. Because the TimeOfDay class derives from the Time interface, the class type is compatible with the formal parameter type Time and, at run time, what is sent over the wire to the server is the TimeOfDay class instance.

1
If you are puzzled by the comment about the year 1753, search the Web for "1752 date change". The intricacies of calendars for various countries prior to that year can keep you occupied for months…

2
It is possible to invoke an operation on a remote class instance—see the relevant language mapping chapter for details.

Table of Contents Previous Next
Logo