How to design an object to get references to other needed objects?


I

itaj sherman

My question concerns some kind of an idiom that is very common in code
I've worked with. Maybe I misuse the word idiom here, I'm not sure. In
code I have seen 3 different designs for such cases. It is quite a
simple problem, but yet, I was never fully content with any of these
3. I tried to think of a way to do it somewhat better. Differences
between these 4 designs (4 together with mine), affect the API of the
module being designed (therefore affect the user's code). Also they
affect the performance and the memory usage. Maybe not by much, but
sometimes I need to cover cases that require high performance.

First I'll describe the module requirements, then the guidelines for
the 4 designs, then I'll explain the differences I find between them
with respect to user code, performance and memory usage. After these I
added a test program with the code for these 4 designs and user code.
If the text here seems to long, you may just skip directly to the
code. It is written not for a real world case, but just has the
necessary to demonstrate the problem. If you read design2 in namespace
ModuleWithMemberReferences and UserCodeWithMemberReferences you'll
understand what the module needs to do, and how my example user wants
to use it. Then you can read design4 in namespace
ModuleWithTemplateParameter and UserCodeWithTemplateParameter to see
how I made it more efficient.

I would like your opinions about these. Do you think my design is
useful at all? Can it be improved?

The module requirements:
Let's say I'm designing a module that has one class A, the API
contains 2 member functions A::f1, A::f2 and 2 global functions g1,
g2, that manipulate objects of A.
basically:

//pseudo code
namespace TheModule
{
class A
{
public: A();

//member functions
public: void f1();
public: void f2();
};

void g1( A& a );
void g2( A& a );
};

Moreover, operations on an object of A need some configuration. During
the lifetime of an object of class A, it needs to use two other
objects, "configuration objects", that would be given by the user of
this module, and will configure certain behaviors of the object of
class A. In here I call them configuration object of type X, and
configuration object of type Y.

So, because the class A needs to somehow refer to these objects, it
has to be a template class, parametrized by the user.
So I change the design to:

//pseudo code
namespace TheModule
{

template< typename MyX, typename MyY >
class A
{
public: A();

//member functions
public: void f1();
public: void f2();
};

template< typename MyX, typename MyY >
void g1( A< MyX, MyY >& a );

template< typename MyX, typename MyY >
void g2( A< MyX, MyY >& a );

};

This design is still incomplete. The problem is that f1,f2,g1,g2 also
need to access an instance of MyX and an instance of MyY. Note, as I
said, that the user of this module is supposed to supply these
objects, and they stay fixed for a certain instance of A for its
lifetime.
The constraint is that I must let the user be able to decide when to
create the instances of MyX and MyY, how many to create, and which of
them will be "attached" to a specific instance of A.

For example: A could represent a node of some tree-like data-
structure, like an XML document. It needs to have a reference to its
direct parent node (that would be MyX), and to an object that
represents the whole document (that would be MyY).
Another example: A is some data-structure. During a lifetime of an
object of type A, it needs to use an object that does the memory
allocation (that would be MyX). This has only 1 configuration object.

I choose to talk about a class that needs 2 configuration objects, to
better demonstrate the effect that it has on the code in the different
designs. For the same reason I choose to have 2 function members and 2
global functions.
In this abstract design, the module expects MyX to have a member
function as such:
public: std::string someXFunctionality();
And expects MyY object to have:
public: std::string someYFunctionality();
But in real situations these complex API and functionality may be
needed.

My question here is how to desine the API of this module to do that.
That is, to enable the user to "attach" instances of MyX and MyY to an
instance of A.
This is what this idiom is about - in what way the user passes these
configuration objects to the module.
Here are the 3 designs I've seen that people use (you can see the full
code of each in the program at the end of my post):

design1: Using parameters. All the functions A::f1, A::f2, g1, g2 and
A::A will receive two more parameters ( ..., MyX& x, MyY& y ), and use
them.

design2: Using reference members. A::A() will receive references to
its configuration objects as: A::A( MyX& x, MyY& y ), and the A
instance will keep these references in reference data members for use
by all other functions of the module.

design3: Using virtual functions. Add two pure virtual member
functions to A, the user will inherit A and override them to return
references to the instances of MyX and MyY. The constructor of A will
still have to receive them as parameters, because it can't use the
virtual functions.
such as:
template< typename MyX, typename MyY >
class A
{
public: A( MyX& x, MyY& y );
public: virtual MyX& vGetMyX() = 0;
public: virtual MyY& vGetMyY() = 0;
...
};
Note that the virtual functions are not used to implement
polymorphism, but just as the means to enable the user to configure
the behavior of the class.

and my suggestion:

design4: Add another template parameter to template class A. such as:
template< typename MyX, typename MyY, typename ObjectParametrization class A
{
...
};

ObjectParametrization will be a traits class that is expected to have
two static member functions that retrieve the instances of MyX and
MyY. The user will have to define such a parameterization class and
implement these functions correctly to retrieve the MyX and MyY
instances he assigned for this instance of A.
such as:

class CertainUserObjectParametrization
{
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
A;

public: static CertainX& GetMyX( A& r );
public: static CertainY& GetMyY( A& r );
};

In fact this design actually decouples the usage of the MyX and MyY
instances which is in the code of this module's function, from the way
this code gets the pointers to these objects.

---------------------

I want to consider a user that wants to use this module in the
following way:
He defines two classes CertainX and CertainY that implement the needed
configuration objects in some certain ways.
He wants to create instances of A, each of them will have its own
instance of CertainX, but all of them will use the same static
allocated instance of CertainY.
Basically the user code will have a class User that will contain an
instance of CertainX and an instace of A< CertainX, CertainY >. He
will also define the static CertainY object.
Then the user wants to add some functionality: he needs two member
functions uf1, uf2 and two global functions ug1,ug2 to manipulate his
objects.

Something like:

namespace UserCode
{
using namespace TheModule;

class CertainX
{
...
};

class CertainY
{
...
};

CertainY s_certainY; //the user wants all instances of A to use this
object.

class User
{
//data
/* here are an instance of CertainX and the instance of A that
will use it. */
private: CertainX _x;
private: A< CertainX, CertainY > _a;

//ctors
public: User();

//member functions
public: void uf1();
public: void uf2();
};

void ug1( User& r );
void ug2( User& r );
}

---------------------

The user of the module here gives one instance of CertainX for each
one instance of A. And a static allocated CertainY for use by all A.
CertainX has 1-1 relation with the instances of A, so they are both
allocated together within a bigger object. I talk about this usage
because this is the one that shows more clearly the differences
between the different designs. The exact location of a static
allocated object and the offset between two sub objects can be
calculated in compile time. Nevertheless designs 2 and 3 don't use
that fact, and cause more indirections at runtime. Design 4 uses the
compile-time calculation of the required
objects and
gets it where it's needed. Design 1 partially does it too, but I think
it's flawed in so many other ways.

Now let’s compare the 4 designs under this usage:

Design 1 requires the user to add MyX and MyY parameters to all calls
of f1,f2,g1,g2.
Designs 1 and 3 don't allow A::~A() to be able to use the instances of
MyX and MyY. That may be necessary in many cases.
Design 2 causes the size of an instance of A to grow, a pointer size
for each configuration object (two is our case).
Design 3 causes A to because a polymorphic type, on most compilers the
virtual table pointer, and performance issues for function calls.
Design 4 has none of the above drawbacks. It does make the user's code
longer in proportion with the number of configuration objects.

look at the following program, and see how each of the designs affects
the user's code.


--------------------

The module's functions f1,f2,g1,g2 are all supposed to do things using
an instance of A and its matching instances of X and Y configuration
objects. So in this program, for demonstration they all call the
template function DoSomething, that just couts a message. In places
where there's a call to DoSomething, just imagine some meaningful code
that uses the parameters given to DoSomething.
Each of the users fucntions uf1,uf2,ug1,ug2 calls one of the module
functions, but not necessarily the one with the matching name. I
crossed calls from member functions to global functions too, to show
the difference in user code.
Design 3 requires the user to inherit A< CertainX, CertainY > in order
to override the virtual functions. In order to construct the CertainX
data member before A<...>, I had to use the private-base member idiom,
with template class BaseMember. The user of design 4 also needed that
in order to do a compile time down cast.
In a few places when calling template functions, I had to explicitly
up-cast an argument to a private base in order to cause correct
parameter type deduction. In these places I used the template function
up_cast (defined in the program). I don't know of a better technique
to do that.



#include <typeinfo>
#include <iostream>


template< typename A, typename X, typename Y >
void DoSomething( std::string callerName, A& a, X& x, Y& y )
{
std::cout << "--DoSomthing form " << callerName << " with "
<< typeid(A).name() << ", "
<< "x=(" << x.someXFunctionality() << ") "
<< "y=(" << y.someYFunctionality() << ")"
<< "\n";
}

/* T should be a reference type, explicitly given by the caller. Don't
use type deduction. */
template< typename T >
T up_cast( T r )
{
/*TODO: add assert that T is a reference type, also verifies that T
was given explicitly */
return r;
}

/* use BaseMember< T > as a private base of your class, if you need a
member T, and you need it constructed in order before some other base
class your class. */
template< typename T >
class BaseMember
{
//data
public: T member;

//ctors
protected: BaseMember()
:
member()
{}

// add more parameter forwarding constructors to member
};


//design 1
namespace ModuleWithFunctionParameters
{

template< typename MyX, typename MyY >
class A
{
//ctors
public: A( MyX& x, MyY& y )
{
DoSomething( "A::ctor", *this, x, y );
}

//member functions
public: void f1( MyX& x, MyY& y )
{
DoSomething( "A::f1", *this, x, y );
}

public: void f2( MyX& x, MyY& y )
{
DoSomething( "A::f2", *this, x, y );
}
};

template< typename MyX, typename MyY >
void g1( A< MyX, MyY >& a, MyX& x, MyY& y )
{
DoSomething( "g1", a, x, y );
}

template< typename MyX, typename MyY >
void g2( A< MyX, MyY >& a, MyX& x, MyY& y )
{
DoSomething( "g2", a, x, y );
}

}

//user of design 1
namespace UserCodeWithFunctionParameters
{
using namespace ModuleWithFunctionParameters;

class CertainX
{
public: std::string someXFunctionality() { return "certain X
functionality"; }
};

class CertainY
{
public: std::string someYFunctionality() { return "certain Y
functionality"; }
};

CertainY s_certainY;

class User;
void ug1( User& r );
void ug2( User& r );

class User
{
friend void ug1( User& r );
friend void ug2( User& r );

//data
private: CertainX _x;
private: A< CertainX, CertainY > _a;

//ctors
public: User()
:
_a( _x, s_certainY )
{}

//member functions
public: void uf1()
{
_a.f1( _x, s_certainY );
}

public: void uf2()
{
g2( _a, _x, s_certainY );
}
};

void ug1( User& r )
{
g1( r._a, r._x, s_certainY );
}

void ug2( User& r )
{
r._a.f2( r._x, s_certainY );
}


}

//design 2
namespace ModuleWithMemberReferences
{

template< typename MyX, typename MyY >
class A
{
//data
MyX& _x;
MyY& _y;

//dtor
public: ~A()
{
DoSomething( "A::dtor", *this, _x, _y );
}

//ctors
public: A( MyX& x, MyY& y )
:
_x(x), _y(y)
{
DoSomething( "A::ctor", *this, x, y );
}

//getters
public: MyX& fGetX()
{
return _x;
}

public: MyY& fGetY()
{
return _y;
}

//member functions
public: void f1()
{
DoSomething( "A::f1", *this, _x, _y );
}

public: void f2()
{
DoSomething( "A::f2", *this, _x, _y );
}
};

template< typename MyX, typename MyY >
void g1( A< MyX, MyY >& a )
{
DoSomething( "g1", a, a.fGetX(), a.fGetY() );
}

template< typename MyX, typename MyY >
void g2( A< MyX, MyY >& a )
{
DoSomething( "g2", a, a.fGetX(), a.fGetY() );
}
}

//user of design 2
namespace UserCodeWithMemberReferences
{
using namespace ModuleWithMemberReferences;

class CertainX
{
public: std::string someXFunctionality() { return "certain X
functionality"; }
};

class CertainY
{
public: std::string someYFunctionality() { return "certain Y
functionality"; }
};

CertainY s_certainY;

class User;
void ug1( User& r );
void ug2( User& r );

class User
{
//data
private: CertainX _x;
private: A< CertainX, CertainY > _a;

friend void ug1( User& r );
friend void ug2( User& r );

//ctors
public: User()
:
_a(_x,s_certainY)
{}

//member functions
public: void uf1()
{
_a.f1();
}

public: void uf2()
{
g2( _a );
}
};

void ug1( User& r )
{
g1( r._a );
}

void ug2( User& r )
{
r._a.f2();
}


}


//design 3
namespace ModuleWithVirtualFunctions
{

template< typename MyX, typename MyY >
class A
{
//ctors
public: A( MyX& x, MyY& y )
{
DoSomething( "A::ctor", *this, x, y );
}

//pure virtuals
public: virtual MyX& vGetMyX() = 0;
public: virtual MyY& vGetMyY() = 0;

//member functions
public: void f1()
{
DoSomething( "A::f1", *this, vGetMyX(), vGetMyY() );
}

public: void f2()
{
DoSomething( "A::f2", *this, vGetMyX(), vGetMyY() );
}
};

template< typename MyX, typename MyY >
void g1( A< MyX, MyY >& a )
{
DoSomething( "g1", a, a.vGetMyX(), a.vGetMyY() );
}

template< typename MyX, typename MyY >
void g2( A< MyX, MyY >& a )
{
DoSomething( "g2", a, a.vGetMyX(), a.vGetMyY() );
}

}



//user of design 3
namespace UserCodeWithVirtualFunctions
{
using namespace ModuleWithVirtualFunctions;

class CertainX
{
public: std::string someXFunctionality() { return "certain X
functionality"; }
};

class CertainY
{
public: std::string someYFunctionality() { return "certain Y
functionality"; }
};

CertainY s_certainY;

class User;
void ug1( User& r );
void ug2( User& r );

class User:
private BaseMember< CertainX >,
private A< CertainX, CertainY >
{
typedef BaseMember< CertainX > XBaseMember;
typedef A< CertainX, CertainY > ABase;

friend void ug1( User& r );
friend void ug2( User& r );

//ctors
public: User()
:
ABase( XBaseMember::member, s_certainY )
{}

//member functions
public: void uf1()
{
ABase::f1();
}

public: void uf2()
{
g2( up_cast<ABase&>(*this) );
}

//overrides for A
virtual CertainX& vGetMyX()
{
return XBaseMember::member;
}

virtual CertainY& vGetMyY()
{
return s_certainY;
}
};

void ug1( User& r )
{
g1( up_cast<User::ABase&>(r) );
}

void ug2( User& r )
{
r.ABase::f2();
}

}


//design 4
namespace ModuleWithTemplateParameter
{

template< typename MyX, typename MyY, typename ObjectParametrization class A
{
//dtors
public: ~A()
{
DoSomething( "A::dtor", *this, ObjectParametrization::GetMyX
(*this), ObjectParametrization::GetMyY(*this) );
}

//ctors
public: A()
{
DoSomething( "A::ctor", *this, ObjectParametrization::GetMyX
(*this), ObjectParametrization::GetMyY(*this) );
}

//member functions
public: void f1()
{
DoSomething( "A::f1", *this, ObjectParametrization::GetMyX
(*this), ObjectParametrization::GetMyY(*this) );
}

public: void f2( )
{
DoSomething( "A::f2", *this, ObjectParametrization::GetMyX
(*this), ObjectParametrization::GetMyY(*this) );
}
};

template< typename MyX, typename MyY, typename ObjectParametrization void g1( A< MyX, MyY, ObjectParametrization >& a )
{
DoSomething( "g1", a, ObjectParametrization::GetMyX(a),
ObjectParametrization::GetMyY(a) );
}

template< typename MyX, typename MyY, typename ObjectParametrization void g2( A< MyX, MyY, ObjectParametrization >& a )
{
DoSomething( "g2", a, ObjectParametrization::GetMyX(a),
ObjectParametrization::GetMyY(a) );
}

}

//user of design 4
namespace UserCodeWithTemplateParameter
{
using namespace ModuleWithTemplateParameter;

class CertainX
{
public: std::string someXFunctionality() { return "certain X
functionality"; }
};

class CertainY
{
public: std::string someYFunctionality() { return "certain Y
functionality"; }
};

CertainY s_certainY;

class User;
void ug1( User& r );
void ug2( User& r );

class CertainUserObjectParametrization
{
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
A;

public: static CertainX& GetMyX( A& r );
public: static CertainY& GetMyY( A& r );
};

class User:
private BaseMember< CertainX >,
private A< CertainX, CertainY, CertainUserObjectParametrization >
{
//A<...> must be a base class, in order to enable down-casting in
the ObjectParametrization traits.
friend class CertainUserObjectParametrization;
typedef BaseMember< CertainX > XBaseMember;
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
ABase;

friend void ug1( User& r );
friend void ug2( User& r );

//ctors
public: User()
:
ABase()
{}

//member functions
public: void uf1()
{
this->ABase::f1();
}

public: void uf2()
{
g2( up_cast<ABase&>(*this) );
}
};

CertainX& CertainUserObjectParametrization::GetMyX( A& r )
{
return static_cast<User&>( r ).BaseMember< CertainX >::member; //
compile time down cast
}

CertainY& CertainUserObjectParametrization::GetMyY( A& r )
{
return s_certainY;
}

void ug1( User& r )
{
g1( up_cast<User::ABase&>(r) );
}

void ug2( User& r )
{
r.ABase::f2();
}

}

namespace WithTemplateParameterUserCleanerCode
{
using namespace ModuleWithTemplateParameter;

class CertainX
{
public: std::string someXFunctionality() { return "certain X
functionality"; }
};

class CertainY
{
public: std::string someYFunctionality() { return "certain Y
functionality"; }
};

CertainY s_certainY;

class User;
void ug1( User& r );
void ug2( User& r );

class CertainUserObjectParametrization
{
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
A;

public: static CertainX& GetMyX( A& r );
public: static CertainY& GetMyY( A& r );
};

class User:
private BaseMember< CertainX >,
private A< CertainX, CertainY, CertainUserObjectParametrization >
{
//A<...> must be a base class, in order to enable down-casting in
the ObjectParametrization traits.
friend class CertainUserObjectParametrization;
typedef BaseMember< CertainX > XBaseMember;
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
ABase;

friend void ug1( User& r );
friend void ug2( User& r );

//ctors
public: User()
:
ABase()
{}

//member functions
public: void uf1()
{
this->ABase::f1();
}

public: void uf2()
{
g2( up_cast<ABase&>(*this) );
}
};

CertainX& CertainUserObjectParametrization::GetMyX( A& r )
{
return static_cast<User&>( r ).BaseMember< CertainX >::member; //
compile time down cast
}

CertainY& CertainUserObjectParametrization::GetMyY( A& r )
{
return s_certainY;
}

void ug1( User& r )
{
g1( up_cast<User::ABase&>(r) );
}

void ug2( User& r )
{
r.ABase::f2();
}

}




int main(int argc, char* argv[])
{

{
std::cout << "ModuleWithFunctionParameters:\n";
{
UserCodeWithFunctionParameters::User a;
a.uf1();
a.uf2();
ug1(a);
ug2(a);
}
std::cout << "ModuleWithMemberReferences:\n";
{
UserCodeWithMemberReferences::User a;
a.uf1();
a.uf2();
ug1(a);
ug2(a);
}
std::cout << "ModuleWithVirtualFunctions:\n";
{
UserCodeWithVirtualFunctions::User a;
a.uf1();
a.uf2();
ug1(a);
ug2(a);
}
std::cout << "ModuleWithTemplateParameter:\n";
{
UserCodeWithTemplateParameter::User a;
a.uf1();
a.uf2();
ug1(a);
ug2(a);
}
}


return 0;
}





program output:

WithFunctionParameters:
--DoSomthing form A::ctor with
N22WithFunctionParameters1AIN26WithFunctionParametersUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f1 with
N22WithFunctionParameters1AIN26WithFunctionParametersUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g2 with
N22WithFunctionParameters1AIN26WithFunctionParametersUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g1 with
N22WithFunctionParameters1AIN26WithFunctionParametersUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f2 with
N22WithFunctionParameters1AIN26WithFunctionParametersUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
WithMemberReferences:
--DoSomthing form A::ctor with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f1 with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g2 with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g1 with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f2 with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::dtor with
N20WithMemberReferences1AIN24WithMemberReferencesUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
WithVirtualFunctions:
--DoSomthing form A::ctor with
N20WithVirtualFunctions1AIN24WithVirtualFunctionsUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f1 with
N20WithVirtualFunctions1AIN24WithVirtualFunctionsUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g2 with
N20WithVirtualFunctions1AIN24WithVirtualFunctionsUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g1 with
N20WithVirtualFunctions1AIN24WithVirtualFunctionsUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f2 with
N20WithVirtualFunctions1AIN24WithVirtualFunctionsUser8CertainXENS1_8CertainYEEE,
x=(certain X functionality) y=(certain Y functionality)
WithTemplateParameter:
--DoSomthing form A::ctor with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f1 with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g2 with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form g1 with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::f2 with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
--DoSomthing form A::dtor with
N21WithTemplateParameter1AIN25WithTemplateParameterUser8CertainXENS1_8CertainYENS1_32CertainUserObjectParametrizationEEE,
x=(certain X functionality) y=(certain Y functionality)
 
Ad

Advertisements

D

Daniel Pitts

itaj said:
My question concerns some kind of an idiom that is very common in code
I've worked with. Maybe I misuse the word idiom here, I'm not sure. In
code I have seen 3 different designs for such cases. It is quite a
simple problem, but yet, I was never fully content with any of these
3. I tried to think of a way to do it somewhat better. Differences
between these 4 designs (4 together with mine), affect the API of the
module being designed (therefore affect the user's code). Also they
affect the performance and the memory usage. Maybe not by much, but
sometimes I need to cover cases that require high performance.
First, there is no silver bullet. There are likely many solutions that
depend on many factors. Sometimes (but less often than you think)
performance trumps. Sometimes, safety and robustness trump. Most of the
time it is a weighting between all constraints.
First I'll describe the module requirements, then the guidelines for
the 4 designs, then I'll explain the differences I find between them
with respect to user code, performance and memory usage. After these I
added a test program with the code for these 4 designs and user code.
If the text here seems to long, you may just skip directly to the
code. It is written not for a real world case, but just has the
necessary to demonstrate the problem. If you read design2 in namespace
ModuleWithMemberReferences and UserCodeWithMemberReferences you'll
understand what the module needs to do, and how my example user wants
to use it. Then you can read design4 in namespace
ModuleWithTemplateParameter and UserCodeWithTemplateParameter to see
how I made it more efficient.
<metacomment>Unless you're writing an introduction to a book or the
abstract of a paper, don't write about what you're going to write about.
We'll figure it out as we read :-) said:
I would like your opinions about these. Do you think my design is
useful at all? Can it be improved?

The module requirements:
Let's say I'm designing a module that has one class A, the API
contains 2 member functions A::f1, A::f2 and 2 global functions g1,
g2, that manipulate objects of A.
basically:

//pseudo code
namespace TheModule
{
class A
{
public: A();

//member functions
public: void f1();
public: void f2();
};

void g1( A& a );
void g2( A& a );
};

Moreover, operations on an object of A need some configuration. During
the lifetime of an object of class A, it needs to use two other
objects, "configuration objects", that would be given by the user of
this module, and will configure certain behaviors of the object of
class A. In here I call them configuration object of type X, and
configuration object of type Y.

So, because the class A needs to somehow refer to these objects, it
has to be a template class, parametrized by the user.
This statement is not necessarily true. You might also use dynamic
polymorphic objects. This may be preferred if binary-size is more
important than runtime-speed. It may also be necessary if the
configurations must be interchangeable at runtime.
So I change the design to:

//pseudo code
namespace TheModule
{

template< typename MyX, typename MyY >
class A
{
public: A();

//member functions
public: void f1();
public: void f2();
};

template< typename MyX, typename MyY >
void g1( A< MyX, MyY >& a );

template< typename MyX, typename MyY >
void g2( A< MyX, MyY >& a );

};

This design is still incomplete. The problem is that f1,f2,g1,g2 also
need to access an instance of MyX and an instance of MyY. Note, as I
said, that the user of this module is supposed to supply these
objects, and they stay fixed for a certain instance of A for its
lifetime.
The constraint is that I must let the user be able to decide when to
create the instances of MyX and MyY, how many to create, and which of
them will be "attached" to a specific instance of A.

For example: A could represent a node of some tree-like data-
structure, like an XML document. It needs to have a reference to its
direct parent node (that would be MyX), and to an object that
represents the whole document (that would be MyY).
Another example: A is some data-structure. During a lifetime of an
object of type A, it needs to use an object that does the memory
allocation (that would be MyX). This has only 1 configuration object.
I choose to talk about a class that needs 2 configuration objects, to
better demonstrate the effect that it has on the code in the different
designs. For the same reason I choose to have 2 function members and 2
global functions.
In this abstract design, the module expects MyX to have a member
function as such:
public: std::string someXFunctionality();
And expects MyY object to have:
public: std::string someYFunctionality();
But in real situations these complex API and functionality may be
needed.

My question here is how to desine the API of this module to do that.
That is, to enable the user to "attach" instances of MyX and MyY to an
instance of A.
The common OO approach to this problem is to have A have a connection to
(reference or pointer) the MyX and MyY objects. If you know that those
objects are available at the time of the A class initialization, then
make them constructor arguments. If not, then make setter methods for them.
This is what this idiom is about - in what way the user passes these
configuration objects to the module.
Here are the 3 designs I've seen that people use (you can see the full
code of each in the program at the end of my post):

design1: Using parameters. All the functions A::f1, A::f2, g1, g2 and
A::A will receive two more parameters ( ..., MyX& x, MyY& y ), and use
them.
This is not OO. It may be valid design for functional programming, but
even then it is dangerous. If A needs x and y to be the same objects
across calls, then you've added burden to the client of your code, along
with greater potential for bugs.
design2: Using reference members. A::A() will receive references to
its configuration objects as: A::A( MyX& x, MyY& y ), and the A
instance will keep these references in reference data members for use
by all other functions of the module.
This is actually the preferred approach.
design3: Using virtual functions. Add two pure virtual member
functions to A, the user will inherit A and override them to return
references to the instances of MyX and MyY. The constructor of A will
still have to receive them as parameters, because it can't use the
virtual functions.
This doesn't really make sense as an approach. Especially since "the
constructor of A will still have to receive them as parameters." Not to
mention it adds a lot of unnecessary coupling between the client and
your code.
such as:
template< typename MyX, typename MyY >
class A
{
public: A( MyX& x, MyY& y );
public: virtual MyX& vGetMyX() = 0;
public: virtual MyY& vGetMyY() = 0;
...
};
Note that the virtual functions are not used to implement
polymorphism, but just as the means to enable the user to configure
the behavior of the class.
This is still polymorphism, but an arguably poor use of it.
and my suggestion:

design4: Add another template parameter to template class A. such as:
template< typename MyX, typename MyY, typename ObjectParametrization
class A
{
...
};

ObjectParametrization will be a traits class that is expected to have
two static member functions that retrieve the instances of MyX and
MyY. The user will have to define such a parameterization class and
implement these functions correctly to retrieve the MyX and MyY
instances he assigned for this instance of A.
such as:

class CertainUserObjectParametrization
{
typedef A< CertainX, CertainY, CertainUserObjectParametrization >
A;

public: static CertainX& GetMyX( A& r );
public: static CertainY& GetMyY( A& r );
};

In fact this design actually decouples the usage of the MyX and MyY
instances which is in the code of this module's function, from the way
this code gets the pointers to these objects.
I think this is an overly complicated approach. It might be useful for
some situation, but I doubt it is any more useful than a constructor or
a pair of setter methods.

Hope my suggestions help.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top