Adt.cpp
// Adt.cpp
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2007
@import "Ceda/cxObject/Object.h"
@import "ExampleUtils.h"
///////////////////////////////////////////////////////////////////////////////////////////////////
/*
Adt
---
A type is a set of values plus a set of operators on those values. The only way to manipulate
values is through the operations that are defined on them. This allows a client to be ignorant of
how the type has been implemented. To emphasise this, the literature uses the term Abstract Data
Type (ADT). Some authors like Chris Date have reasonably argued that this terminology is
superfluous because by definition every type is an ADT.
In any case, it is convenient to use the term ADT to refer to something quite specific : The old
C programming idiom of forward declaring a struct in a header, together with a set of (global)
functions that take pointers to the forward declared struct.
For example
////////// CDPlayer.h
struct CDPlayer;
CDPlayer* CreateCDPLayer();
void Eject(CDPlayer*);
void Play(CDPlayer*);
void Stop(CDPlayer*);
void Skip(CDPlayer*);
void Destroy(CDPlayer*);
This is an excellent idiom. It is simple and convenient, very efficient and completely hides the
implementation. It achieves the ultimate in binary decoupling.
We support a syntactically convenient way of writing the above code, as follows
$adt CDPlayer
{
void Eject();
void Play();
void Stop();
void Skip();
void Destroy();
};
CDPlayer* CreateCDPLayer();
C++ allows for name overloading, therefore it is reasonable to use simple names for global
functions that represent operations on an ADT. Simple naming conventions allow for code reuse
through templates - such as the following AutoDestroy<T> class
template<typename T>
class AutoDestroy
{
public:
AutoDestroy(T* p) : p_(p) {}
~AutoDestroy() { Destroy(p_); }
private:
T* p_;
};
Comparison to alternative approaches
------------------------------------
The most usual approach in C++ is to write a CDPlayer class definition in the header.
class CDPlayer
{
public:
CDPlayer();
~CDPlayer();
void Eject();
void Play();
void Stop();
void Skip();
private:
...
};
Although the implementation would normally be "private", it leads to binary coupling. This
approach is particularly suitable for simple types like Point and Circle because C++ allows for
these types to be used as "value types" and the operations can be fully inlined.
For a more complex type like a CDPlayer it can be desirable to avoid exposing the implementation
in a public header. Reasons include
* Simpler public headers
* Protection of IP
* Faster compilation by reducing dependencies
* Binary decoupling - allowing DLLs to have independent upgrade paths.
Binary coupling can be particularly serious if it exposes (for example) STL implementations because
the STL is not a reliable binary standard for interoperability.
A common way to achieve this in C++ is to define a pure abstact base class. Eg
struct ICDPlayer
{
virtual void Eject() = 0;
virtual void Play() = 0;
virtual void Stop() = 0;
virtual void Skip() = 0;
virtual void Destroy() = 0;
};
ICDPlayer* CreateCDPLayer();
This approach is practical and works well. Ignoring simple syntactic differences, the following
are some pros and cons compared to the C style of writing an ADT.
pros of abstract interface
* Run-time polymorphism allows the CDPlayer to be implemented in multiple ways at
run time.
cons of abstract interface
* We have to have two types : ICDPlayer and CDPlayer
* Every method call involves indexing into a vtable so it is slightly less efficient
* CDPlayer objects have the space overhead of a vtable pointer
* The compiler has to generate a VTable
*/
namespace Adt1
{
/////////////// CDPlayer.h
$adt CDPlayer
{
void Play();
void Destroy();
};
CDPlayer* CreateCDPLayer();
/////////////// client.cpp
void Run()
{
ceda::TraceGroup g("Adt example 1");
CDPlayer* p = CreateCDPLayer();
Play(p);
Destroy(p);
}
/////////////// CDPlayer.cpp
$adt implement CDPlayer
{
void Play() { Tracer() << "Play called\n"; }
void Destroy() { delete this; }
};
CDPlayer* CreateCDPLayer()
{
return new CDPlayer;
}
}
/*
The following example, shows the usage of $implementing which is useful for
automatically declaring all the method prototypes of an adt
It also shows how '+' would normally be used to reflect and export the adt and the
function CreateCDPLayer(). This allows the adt to be accessed from other DLLs or
from Python.
*/
namespace Adt2
{
/////////////// CDPlayer.h
$adt+ CDPlayer
{
void Play();
void Destroy();
};
$function+ CDPlayer* CreateCDPLayer();
/////////////// client.cpp
void Run()
{
ceda::TraceGroup g("Adt example 2");
CDPlayer* p = CreateCDPLayer();
Play(p);
Destroy(p);
}
/////////////// CDPlayer.cpp
$adt+ implement CDPlayer
{
$implementing adt CDPlayer;
};
void CDPlayer::Play() { Tracer() << "Play called\n"; }
void CDPlayer::Destroy() { delete this; }
$function+ CDPlayer* CreateCDPLayer()
{
return new CDPlayer;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
namespace Adt
{
void Run()
{
Adt1::Run();
Adt2::Run();
}
}