Free Store and Object Allocation
An abstract object cannot be handled that way, because its layout is deliberately kept out of view.
Two Things Every Type Offers
When you use a type, you usually care about two different things: what you can ask it to do, and how it stores its data internally. Most of the time those two things are tangled together. The type exposes its layout, and the code you write depends on that layout.
An abstract type is built to pull these two things apart. It is designed so that a user can work with it without knowing anything about how it is put together inside. This article introduces that idea, shows how it differs from the more familiar kind of type, and explains the practical consequences that follow from it.
What do you think happens?
Does a user of an abstract type need to know how big its objects are?
Reveal answer
No. The representation of an abstract type is unknown to the user, and that includes its size.
This one fact drives the rest of the article. If the size is unknown, the language cannot place the object in a plain local variable whose layout the surrounding code understands, so the object has to live on the free store and be reached through a reference or pointer.
Tracing What a Variable Holds
Start by asking a simple question about memory. When the user writes a variable of an abstract type, does that variable contain the object itself? According to the source, it does not. The variable holds a way of reaching the object, and the object holds the representation. The diagram below traces this arrangement from the user's variable down to the hidden layout.
Compare this with the picture for a concrete type. A local variable of a concrete type is handled as a value: the value lives in the scope where it is declared, and the surrounding code understands its layout. An abstract object cannot be handled that way, because its layout is deliberately kept out of view. The variable therefore becomes a handle on the object rather than the object itself.
Concrete Types Show Their Layout
The familiar kind of type is called a concrete type. Types such as complex and Vector are examples. Their representation is part of their definition: the way the data is laid out is written directly into the type's definition, and users can see it. Because of this, concrete types resemble the built-in types that come with the language.
A concrete type is a type whose representation is part of its definition. Its layout is visible to users, and it resembles the built-in types of the language.
The key test for telling the two kinds apart is a single question: is the representation part of the type's definition? If the answer is yes, the type is concrete. If the representation has been deliberately removed from the definition as seen by the user, the type is abstract. The table below lines up the two kinds against this test.
| Feature | Concrete type | Abstract type |
|---|---|---|
| Representation in the definition | Part of the definition and visible to the user | Deliberately removed from the user's view |
| Examples named in the source | complex and Vector | A Container class, intended as a more abstract version of Vector |
| What a user can depend on | The layout, because it is visible | Only the interface, because the representation and even its size are unknown |
| Local variables | Genuine local variables are available | Genuine local variables are given up |
| How objects are held | Handled like built-in value types | Allocated on the free store and accessed through references or pointers |
Concrete and abstract types differ in whether the representation belongs to the type's definition.
Interface and Representation
The source describes the mechanism for insulation in one short phrase: the design decouples the interface from the representation. These two parts need to be separated clearly before the idea makes sense.
The interface of a type is the set of operations a user can perform on it. The representation is the way the data is actually stored.
Decoupling means these two parts are kept separate. The user's code is written against the interface alone, and the representation can be chosen or changed without disturbing that code. The diagram shows the user's code reaching the interface, while the representation sits behind it and is never reached directly.
The source develops this with a class called Container, intended as a more abstract version of the Vector type seen earlier. Notice the order of work. The interface is designed first, and at that stage only the interface is fixed. Questions about how the elements are stored come later, and they do not leak into the description of what the user may do.
What a User Can Rely On
Decoupling changes what a user is entitled to depend on. Before the representation is hidden, user code can depend on how the data is laid out. After it is hidden, user code can depend only on the operations in the interface. The diagram compares the two situations side by side.
The practical payoff follows directly. Because the user's code relies only on the operations, the internal storage can be replaced without the user having to rewrite anything that depends only on those operations. The user's dependency is on what the type can do, not on how the type stores its data.
Why Abstract Objects Live on the Free Store
Since the representation of an abstract type is unknown to the user, not even its size is known. The language design therefore has to avoid any situation in which the compiler or the user's code needs to know that size in advance. The source draws the consequence clearly: objects of an abstract type must be allocated on the free store and accessed through references or pointers.
The free store is the region used for objects whose lifetime is managed explicitly rather than tied to a single scope. References and pointers are the means by which the user's code refers to such objects.
This explains the whole arrangement. A variable of an abstract type cannot be a container for the object, because the object's size is not known where the variable is declared. The variable instead holds a reference or pointer, and the object lives on the free store, where it holds the representation.
Working With a Container Through a Reference
A user wants to work with a Container, an abstract type whose interface was designed before its storage was decided. Trace what the user's code must hold, and what it must not assume.
- 1Start from the interface
The user knows what Container can do, because the interface was fixed first. The user does not know how the elements are stored.
- 2Check whether the size is needed
The size of a Container is part of the unknown representation. So the user's code cannot reserve room for a Container object in a local variable.
- 3Hold a reference or pointer
The variable holds a way of reaching an object allocated on the free store. The user calls the interface operations through that reference.
- 4Avoid assuming the layout
The user never reads the internal storage. If the storage is later changed, the code that calls only the interface still works.
The user works with Container entirely through its interface, holding a reference to an object on the free store, and never depends on the representation or its size.
The Cost of Hiding the Representation
Hiding the representation has a cost, and the source names it directly: the approach gives up genuine local variables. A local variable is a value that lives in the scope where it is declared and is handled as a value. To keep the representation out of view, the design cannot treat an abstract object as a plain value whose layout the surrounding code understands.
The trade is deliberate. The source accepts these restrictions so that the user can work with the type without seeing its internal layout. Losing the convenience of local variables is the price of the insulation the design provides.
Mistakes Beginners Make
Common mistake Assuming an abstract type's size is part of what the user can rely on
Why it failsThe representation of an abstract type is unknown to the user, and that includes its size.
Do this insteadRely only on the interface operations, and never plan memory around a size the user has not been given.
Common mistake Treating an abstract object like a concrete type such as complex, and holding it in a plain local variable
Why it failsA concrete type's representation is part of its definition, but an abstract object gives up genuine local variables.
Do this insteadAllocate the object on the free store and hold it through a reference or pointer.
Common mistake Reading the internal storage to decide what operations are available
Why it failsThe operations are the interface, which is designed first and kept separate from how elements are stored.
Do this insteadDecide what to do from the interface alone, so the code keeps working if the storage changes.
Common mistake Believing that the free store and a single scope manage object lifetime in the same way
Checking Your Understanding
A type is described as having its layout written directly into its definition, with users able to see that layout. Another type keeps its representation hidden, and users know only its operations. Which of the two is concrete, which is abstract, and why does the answer depend on whether the representation is part of the definition?
Hints
- Ask whether the representation is part of the type's definition.
- A concrete type resembles the built-in types, such as complex and Vector.
- An abstract type's objects must be reached through references or pointers.
Key Takeaways
- An abstract type insulates the user from implementation details by decoupling the interface from the representation, so the user depends on what the type can do rather than how it stores data.
- A concrete type has its representation as part of its definition, as with complex and Vector, and resembles the built-in types. An abstract type removes the representation from the user's view.
- Because the representation of an abstract type is unknown, not even its size, its objects must be allocated on the free store and accessed through references or pointers.
- The cost of this design is that genuine local variables of abstract types are given up, and the internal storage can then be changed without breaking code that relies only on the interface.