Since more users are likely to be familiar with C than C++, this section provides a very brief introduction to the essentials of C++.
The central feature of C++ is that it extends the struct
construct of C into a full-fledged object oriented programming (OOP)
language based around a class
. Thus, a class or object has data
members, like a struct
in C, but it also has functions associated
with it. These member functions or methods perform
various functions associated with the data members, and together the
whole thing ends up encapsulating a set of related tasks or operations
together in a single entity.
The object-oriented notion is very intuitive for neural-network entities like units:
class Unit { // this defines an object of type Unit public: // public means that any other object can access the following // these are the data members, they are stored on the object float net_input; // this gets computed by the weights, etc. float activation; // this is computed by the function below float target; // this is set by the environment before hand float error; // this gets computed by the function below // these are the member functions, they can easily access the data members // associated with this object virtual void Compute_Activation() { activation = 1.0 / (1.0 + exp(net_input)); } virtual void Compute_Error() { error = target - activation; error *= error; } };
The member functions encapsulate some of the functionality of the unit
by allowing the unit to update itself by calling its various member
functions. This is convenient because different types of units might
have different definitions of these functions, but they all would
have a Compute_Activation()
function that would perform this same
basic operation. Thus, if you have an object which is an instance or
token of the type Unit
, you can set its data members and call its
member functions as follows:
Unit un; un.net_input = 2.5; un.target = 1.0; un.Compute_Activation(); un.Compute_Error();
Thus, you use the same basic notation to access data members as member
functions (i.e., the obj.mbr
syntax). To illustrate why this
encapsulation of functions with objects is useful, we can imagine
defining a new object type that is just like a unit but has a different
activation function.
class TanhUnit : public Unit { // we're going to inherit from a Unit public: void Compute_Activation() { activation = tanh(net_input); } };
This notation means that the new type, TanhUnit
, is just like a
Unit
, except that it has a different version of the
Compute_Activation()
function. This is an example of
inheritance, which is central to C++ and OOP in general. Thus,
if we have something which we know to be a unit of some type (either a
Unit
or a TanhUnit
, or maybe something else derived from a
Unit
), we can still call the Compute_Activation()
function
and expect it to do the right thing:
Unit* un; un->Compute_Activation();
This is an example of a virtual member function, because the
actual function called depends on what actual type of unit you have.
This is why the original definition of the Compute_Activation()
function has the virtual
keyword in front of it. Virtual
functions are an essential part of C++, as they make it possible to have
many different definitions or "flavors" of a given operation. Since
these differences are all encapsulated within a standard set of virtual
functions, other objects do not need to have special-case code to deal
with these differences. Thus, a very general purpose routine can be
written which calls all of the Compute_Activation()
functions on
all of the units in a network, and this code will work regardless of
what actual type of units are in the network, as long as they derive
from the base type of Unit
.
While there are a number of other features of C++, the PDP++ software mainly makes use of the basics just described. There are a couple of basic object types that are used in the software for doing file input/output, and for representing character-string values.
The C++ way of doing file input/output is via the stream concept.
There is an object that represents the file, called a stream object.
There are different flavors of stream objects depending on whether you
are doing input (istream
), output (ostream
) or both
(iostream
). To actually open and close a file on a disk, there
is a version of the iostream
called an fstream
that has
functions allowing you to open and close files. To send stuff to or
read stuff from a file, you use something like the "pipe" or i/o
redirection concept from the standard Unix shells. The following
example illustrates these concepts:
fstream fstrm; // fstrm is a file stream, which can do input or output // we are opening the file by calling a member function on the // fstream object. the enumerated type ios::out means 'output' // also available are ios::in, ios::app, etc. fstrm.open("file.name", ios::out); // we "pipe" stuff to the fstrm with the << operator, which can deal // with all different types of things (ints, floats, strings, etc.) fstrm << "this is some text " << 10 << 3.1415 << "\n"; fstrm.close(); // again, the file is closed with a member fun
The mode in which the file is opened is specified by an enumerated
type or an enum
. This provides a way of giving a descriptive
name to particular integer values, and it replaces the use of
string-valued arguments like "r" and "w" that were used in the
open()
function of the standard C library. The base class of all
the stream types is something called ios
, and the enum for the
different modes a file can be opened in are defined in that type, which
is why they are scoped to the ios
class by the
ios::
syntax. The definition of this enum in ios is as follows:
enum open_mode { in=1, out=2, ate=4, app=010, trunc=020, nocreate=040, noreplace=0100 };
which shows that each enum value defines a bit which can be combined with others to affect how the file is opened.
The following example illustrates how file input works with streams.
One simply uses the >>
operator instead of <<
. Note that
the fstream has to be opened in ios::in
mode as well:
fstream fstrm; // fstrm is a file stream, input or output // we are opening the file by calling a member function on the // fstream object. the enumerated type ios::out means 'output' // also available are ios::in, ios::app, etc. fstrm.open("file.name", ios::in); // these variables will hold stuff that is sucked in from the stream String words[4]; int number; float pi; // this assumes that the stream was written by the output example // given previously fstrm >> words[0] >> words[1] >> words[2] >> words[3] >> number >> pi; fstrm.close(); // again, the file is closed with a member fun