Encapsulation
Steven Zeil
1 Encapsulation
The ADT as Contract
When an ADT specification/implementation is provided to users (application programmers):
-
Users are expected to alter/examine values of this type only via the members specified.
-
The creator of the ADT promises to leave the member specifications unchanged.
Enforcing the Contract
-
If we leave it like this, then the contract is just a “gentlemen’s agreement”.
-
Programmers in older programming languages do just that.
-
And violations of the agreement were common, leading to buggy code.
-
-
Programming language designers eventually noticed, and introduced new language features…
-
-
Encapsulation is the use of programming language rules to enforce information hiding design decisions.
1.1 Encapsulation in C++
We can mark portions of a struct
as
-
public , meaning that names declared there can be used by any code.
-
private , meaning that names declared there can only be used by code that is part of the struct.
Time for Encapsulation
struct Time {
public:
// Create time objects
Time(); // midnight
Time (int h, int m, int s);
// Access to attributes
int getHours();
int getMinutes();
int getSeconds();
// Calculations with time
void add (Time delta);
Time difference (Time fromTime);
/**
* Read a time (in the format HH:MM:SS) after skipping any
* prior whitepace.
*/
void read (std::istream& in);
/**
* Print a time in the format HH:MM:SS (two digits each)
*/
void print (std::ostream& out);
/**
* Compare two times. Return true iff time1 is earlier than or equal to
* time2
*/
bool noLaterThan(const Time& time2);
/**
* Compare two times. Return true iff time1 is equal to
* time2
*
*/
bool equalTo(const Time& time2);
private:
// From here on is hidden
int secondsSinceMidnight;
};
int Time::getHours()
{
return secondsSinceMidnight / 3600; // OK
}
int getHours(Time t)
{
return t.secondsSinceMidnight / 3600; // Error
}
2 Classes
From structs to classes
-
Most ADTs in C++ are written as
class
es, notstruct
s. -
Almost identical, according to language rules
Until you explicitly say “public:” or “private:”,
-
Structs start out as “public”
-
Classes start out as “private”
-
-
Very different in style/idiom
-
Classes are used for “real” ADTs that the application coders will work with.
-
Structs are used for “helper” types that the classes uses to get their work done
-
often nested inside classes
-
or for such trivial structured data that no information hiding is necessary
-
Showing a Little Class
Our time class:
class Time{
public:
// Create time objects
Time(); // midnight
Time (int h, int m, int s);
// Access to attributes
int getHours();
int getMinutes();
int getSeconds();
// Calculations with time
void add (Time delta);
Time difference (Time fromTime);
/**
* Read a time (in the format HH:MM:SS) after skipping any
* prior whitepace.
*/
void read (std::istream& in);
/**
* Print a time in the format HH:MM:SS (two digits each)
*/
void print (std::ostream& out);
/**
* Compare two times. Return true iff time1 is earlier than or equal to
* time2
*/
bool noLaterThan(const Time& time2);
/**
* Compare two times. Return true iff time1 is equal to
* time2
*
*/
bool equalTo(const Time& time2);
private:
// From here on is hidden
int secondsSinceMidnight;
};
And our alternate version of the same class:
class Time {
// The declaration below is hidden
int secondsSinceMidnight;
public:
// Create time objects
Time(); // midnight
Time (int h, int m, int s);
// Access to attributes
int getHours();
int getMinutes();
int getSeconds();
// Calculations with time
void add (Time delta);
Time difference (Time fromTime);
/**
* Read a time (in the format HH:MM:SS) after skipping any
* prior whitepace.
*/
void read (std::istream& in);
/**
* Print a time in the format HH:MM:SS (two digits each)
*/
void print (std::ostream& out);
/**
* Compare two times. Return true iff time1 is earlier than or equal to
* time2
*/
bool noLaterThan(const Time& time2);
/**
* Compare two times. Return true iff time1 is equal to
* time2
*
*/
bool equalTo(const Time& time2);
};
3 Hiding Attributes
An almost universal rule of thumb for ADTs in C++:
All data members should be private.
-
Attributes are data values that we regard as “contained” in our abstraction
-
They are part of the mental model, must be part of the interface
-
-
If a data member represents an attribute, can we hide it?
-
Yes, but we provide access via get/set functions.
-
Example: Time Attributes
class Time {
public:
// Create time objects
Time(); // midnight
Time (int h, int m, int s);
// Access to attributes
int getHours();
void setHours (int h);
int getMinutes();
void setMinutes (int m);
int getSeconds();
void setSeconds (int s);
// Calculations with time
void add (Time delta);
Time difference (Time fromTime);
/**
* Read a time (in the format HH:MM:SS) after skipping any
* prior whitepace.
*/
void read (std::istream& in);
/**
* Print a time in the format HH:MM:SS (two digits each)
*/
void print (std::ostream& out);
/**
* Compare two times. Return true iff time1 is earlier than or equal to
* time2
*/
bool noLaterThan(const Time& time2);
/**
* Compare two times. Return true iff time1 is equal to
* time2
*
*/
bool equalTo(const Time& time2);
private:
// From here on is hidden
int hours;
int minutes;
int seconds;
};
int Time::getHours()
{
return hours;
}
void Time::setHours(int h)
{
hours = h;
}
4 Inline Functions
Are Short Functions Ineffcient?
Are ADT implementations terribly inefficient?
They tend to feature a lot of one-liners and similar short functions.
int Time::getHours()
{
return hours;
}
4.1 How Functions Work
int foo(int a, int b)
{
return a+b-1;
}
would compile into a block of code equivalent to
stack[1] = stack[3] + stack[2] - 1;
jump to address in stack[0]
The Runtime Stack
-
the “stack” is the runtime stack (a.k.a. the activation stack) used to track function calls at the system level,
-
stack[0] is the top value on the stack,
-
stack[1] the value just under that one, and so on.
4.2 How Function Calls Work
A function call like
x = foo(y,z+1);
would be compiled into a code sequence along the lines of
push y onto the runtime stack;
evaluate z+1;
push the result onto the runtime stack
push (space for the return value) onto the runtime stack
save all CPU registers
push address RET onto the runtime stack
jump to start of foo's body
RET: x = stack[1]
pop runtime stack 4 times
restore all CPU registers
As you can see, there’s a fair amount of overhead involved in passing parameters and return address information to a function when making a call.
-
The amount of time spent on this overhead is really all that large.
-
If the function body contains several statements of any kind of loop, then the overhead is probably a negligable fraction of the total time spent on the call.
Overhead
int Time::getHours()
{
return hours;
}
void Time::setHours(int h)
{
hours = h;
}
-
For functions like these, the overhead may be greater than the time needed for the function body itself
-
If called inside a loop that repeats many times, delay can be significant
4.3 Inline Functions
class Time {
public:
// Create time objects
Time(); // midnight
Time (int h, int m, int s);
// Access to attributes
int getHours() {return hours;}
void setHours (int h) {hours = h;}
int getMinutes();
void setMinutes (int m);
⋮
private:
int hours;
int minutes;
int seconds;
};
inline
int Time::getMinutes()
{
return minutes;
}
inline
void Time::setMinutes(int m)
{
minutes = m;
}
- C++ offers the option of declaring functions as inline.
-
can be written one of two ways.
-
inside the class declaration.
-
or place the reserved word inline in front of the function definition written outside the class declaration.
-
-
How Inline Functions Work
When we make a call to an inline function, the compiler simply replaces the call by a compiled copy of the function body (with some appropriate renaming of variables to avoid conflicts).
Example of an Inline Call
If we have
inline int foo(int a, int b)
{
return a+b-1;
}
and we later make a call
x = foo(y,z+1);
This would be compiled into a code sequence along the lines of
evaluate z+1, storing result in tempB
evaluate y + tempB - 1, storing result in x
Most of the overhead of making a function call has been eliminated.
**Use Inline For Functions That Are … **
-
short
-
called many times
If abused, inline calls tend to make the size of the executable program grow.
Inlining is Optional
Inlining is only a recommendation from the programmer to the compiler.
- The compiler may ignore an inline declaration if it prefers.
-
inlining of functions with recursive calls is impossible
-
Many compilers will refuse to inline any function whose body contains a loop.
-
5 Example: the Auction Program as Encapsulated ADTs
#include <iostream>
#include <string>
using namespace std;
#include "items.h"
#include "itemcollection.h"
#include "bidders.h"
#include "biddercollection.h"
#include "bids.h"
#include "bidcollection.h"
#include "time.h"
const int MaxBidders = 100;
const int MaxBids = 5000;
const int MaxItems = 100;
/**
* Determine the winner of the auction for item #i.
* Announce the winner and remove money from winner's account.
*/
void resolveAuction (const Item& item,
BidderCollection& bidders,
BidCollection& bids);
int main (int argc, char** argv)
{
if (argc != 4)
{
cerr << "Usage: " << argv[0] << " itemsFile biddersFile bidsFile" << endl;
return -1;
}
ItemCollection items (MaxItems);
items.readItems (argv[1]);
BidderCollection bidders (MaxBidders);
bidders.readBidders (argv[2]);
BidCollection bids (MaxBids);
bids.readBids (argv[3]);
for (int i = 0; i < items.getSize(); ++i)
{
resolveAuction(items.get(i), bidders, bids);
}
return 0;
}
/**
* Determine the winner of the auction for an item.
* Announce the winner and remove money from winner's account.
*/
void resolveAuction (const Item& item,
BidderCollection& bidders,
BidCollection& bids)
{
double highestBidSoFar = 0.0;
string winningBidderSoFar;
bool reservePriceMet = false;
for (int bidNum = 0; bidNum < bids.getSize(); ++bidNum)
{
Bid bid = bids.get(bidNum);
if (bid.getTimePlacedAt().noLaterThan(item.getAuctionEndTime()))
{
if (bid.getItem() == item.getName()
&& bid.getAmount() > highestBidSoFar
)
{
int bidderNum = bidders.findBidder(bid.getBidder());
Bidder bidder = bidders.get(bidderNum);
// Can this bidder afford it?
if (bid.getAmount() <= bidder.getBalance())
{
highestBidSoFar = bid.getAmount();
winningBidderSoFar = bid.getBidder();
}
}
if (bid.getAmount() > item.getReservedPrice())
reservePriceMet = true;
}
}
// If highestBidSoFar is non-zero, we have a winner
if (reservePriceMet && highestBidSoFar > 0.0)
{
int bidderNum = bidders.findBidder(winningBidderSoFar);
cout << item.getName()
<< " won by " << winningBidderSoFar
<< " for " << highestBidSoFar << endl;
Bidder& bidder = bidders.get(bidderNum);
bidder.setBalance (bidder.getBalance() - highestBidSoFar);
}
else
{
cout << item.getName()
<< " reserve not met"
<< endl;
}
}
-
The Bid ADT: \linklisting{../encapsulation-auction/bids.h} and \linklisting{../encapsulation-auction/bids.cpp}.
-
The Bidder ADT: \linklisting{../encapsulation-auction/bidders.h} and \linklisting{../encapsulation-auction/bidders.cpp}.
-
The Item ADT: \linklisting{../encapsulation-auction/items.h} and \linklisting{../encapsulation-auction/items.cpp}.
-
The Time ADT: \linklisting{../encapsulation-auction/time.h} and \linklisting{../encapsulation-auction/time.cpp}.
-
The Money ADT:
Note how the vast majority of the code is now a collection of encapsulated ADTs, with only a limited amount of very application-specific code left outside.
This is very typical of well-written C++ applications.