When designing a class hierarchy, you may face a decision between inheritance (aka IsA ) vs. containment (aka HasA) relation. For instance, if you are designing a
Radio
class, and you already have the following classes implemented for you in some library:
Dial, ElectricAppliance.
It is quite obvious that your Radio should be derived from
ElectricAppliance.
However, it is not so obvious that
Radio
should also be derived from
Dial
. How to decide? You can check whether there is always a 1:1 relation between the two, e.g., do all radios have one and only one dial? You may realize that the answer is "no": a radio can have no dial at all (a transmitter/receiver adjusted to a fixed frequency) or may have more than one (both an FM dial and AM dial). Hence, your
Radio
class should be designed to have Dial(s) instead of being derived from it. Note that the relation between
Radio
and
ElectricAppliance
is always 1:1 and corroborates the decision to derive
Radio
from
ElectricAppliance
115. Static member function
A static member function in a class can access only other static members of a class or variables which are not members of its class. It can be invoked even without an object instance, unlike any other member functions:
class stat{
int num;
public:
stat(int n = 0) {num=n;}
static void print() {cout <<"static member function" <<endl;
};
void main() {
stat::print(); //no object instance required
stat s(1);
s.print(); //but still can be called from an object
}//end main
Static members are used when all other data members of an object are also static; when the function does not depend on any other object member (like print() above); or simply when a global function is undesirable so it is wrapped in a class.
116. Calling an object's member function from its constructor
An object's member functions (both virtual and non-virtual) can be called from its constructor. The invoked virtual is guaranteed to be the one defined in the current object (or higher, if it has not been overridden in the current object). However, virtuals of objects derived from the one whose constructor is being executed are not called.
class A {
public:
virtual void f() {}
virtual void g() {}
};
class B: public A {
public:
void f () {} //overriding A::f()
B() { f(); //calls B::f()
g(); //g() not overriden in B, therefore calling A::g() }
};
Mind that if the object's member functions use object data members, it is the implementor's responsibility to initialize them first, preferably by a mem-initializer list:
class C {
int n;
int getn() const { cout<<n<<endl; }
public:
C(int j) : n(j) { getn(); } //Fine: n initialized before getn()
//is called; otherwise - n would
//have an undefined value
};
117. The Explicit Keyword
A constructor taking a single argument is by default an implicit conversion operator:
class C {
int I;
//...
public:
C(int i);//constructor and implicit conversion operator
//as well
};
void f() {
C c(0);
c = 5; //implicit conversion of 5 to a C object and
//then assignment
}
The compiler re-edits the above sample as if the programmer had written:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//"c=5;" transformed by the compiler into something like this:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
C temp(5);//temporary object instantiated,
c = temp; //assigned using operator =
temp.C::~C(); //temp's destructor activated
In many cases, this conversion is intentional and well-behaved. But there are cases where such automatic conversion is undesirable, like the following:
class String {
int size;
char *p;
//..
public:
String (int sz); //constructor & implicit conversion
//operator; undesirable in this case
};
void f ()
{
String s(10);
//the following is a programmer's typo; yet it is not
//detected since it has an unexpected interpretation:
s = 100; //bad; 100 is converted to a String and then
//assigned to s.
}
In order to avoid such implicit conversions, a constructor taking one argument should be declared as explicit:
class String {
int size;
char *p;
//..
public:
//no implicit conversion
explicit String (int sz); //no implicit conversion
String (const char *s, int size n = 0); //implicit conv.
};
void f ()
{
String s(10);
s = 100; //now compile time error; explicit conversion
//required now:
s = String(100); //fine; explicit conversion
s = "st";//fine; implicit conversion allowed in this case
}
118. That's what friends are for
A class can declare external functions or other classes as friends. Friendship grants full access to all of the grantor's members, even private and protected ones:
void encrypt (string & rep) {/*..*/} //global function
class spy {
public:
static void transmit(const string& rep) { /*..*/}
//...
};
class secret {
friend class spy;//spy can access all members of 'secret'
friend void encrypt(string & rep);//...and so can encrypt
private:
string report;
public:
void scramble() { ::encrypt(report); }
void transmit() const { spy::transmit(report); }
};
Notes about friendship:
1. A friend declaration exposes implementations details of its class, so it should be used wisely. However, friendship has the advantage of allowing code re-use in a simple manner; in fact, many of the standard functions and overloaded operators are used in standard containers (like string<>) by means of friendship.
2. Friendship is not inherited, so non-public members of any class derived from secret are not accessible to spy and encrypt.
119. A static class member
A class member declared static is a single instance of that member shared by all instances of this class (that's why it is sometimes termed a class variable, as opposed to object variable). This feature has many uses: for instance, in order to create a file lock, one can use a static bool class member. An object trying to access this file has to check first whether the static (i.e., shared) flag is false. If it is, the object turns the flag on and processes the file safely, since other objects will now find that the flag is now on, and hence -- they cannot access the file. When the object processing the file is done, it has to turn off the flag, enabling another object to access it. How is a static member created?
class fileProc {
FILE *p;
static bool isLocked; //only a declaration; see definition below...
public:
bool isLocked () const {return isLocked; }
};
//somewhere outside the class definition:
bool fileProc::isLocked; //definition; initialized to 'false' by default. Note: no 'static' here
120. Prefer dynamic_cast<> to typeid()
A robust, long lasting OO design relies on the premise that an existing class can be re-used in the future by means of derivation. Therefore, examining the actual type of an object with typeid() results in a code which is less flexible, since it cannot handle derived objects:
void Registry::Register (const Window& wind) //has to receive a Window object exclusively
{
if (typeid(wind) == typeid(Window)) //inflexible; objects derived from Window will fail this test
{
Store ( wind.GetHandle() );
}
else //object derived from Window was received; not handled
{
cout<< "Window object expected!"<<endl;
}
}
The use of dynamic_cast <> rather than typeid() is a better choice - it will enable the Registry::Register() member function to cope with a Window object as well as any object derived from it:
void Registry::Register (const Window& wind) //has to receive a Window object exclusively
{
Window w=dynamic_cast<Window&>> (wind) //will succeed even with derived objects
{
Store ( w.GetHandle() ); //it is guaranteed that Window::GetHandle() is called
}
}
121. The Type of a Class Template
A template name is not a type. When you need a template functioning as a full type (for example: to serve as a base class or as a function argument), you have to specify its arguments:
size_t n = sizeof(vector); //compile time error; template argument list is required
size_t n = sizeof(vector<int>); //fine
class myCharVector : public vector<char> { //also fine
//...
};
122. Standard vs. User Defined Conversions in Overloaded Function Call
A non-explicit constructor taking a single argument is also a conversion operator, which casts its argument to an object of the constructor's class. When the compiler has to resolve an overloaded function call, it takes into consideration this user-defined cast:
class Numeric {
float f;
public:
Numeric(float ff): f(ff) {}//constructor is also a float-to-Numeric conversion operator
};
void f(Numeric&); //function prototype
Numeric num(0.05); //object
f(5f); //calls void f(Numeric&). Numeric's constructor converts argument to a Numeric object
However, suppose we add a new overloaded version of f:
void f (double);
Now the same function call will resolve differently:
f(5f); //calls f(double); standard conversions have higher precedence than user-defined ones
This is because float variables are converted to double automatically in order to match an overloaded function signature. This type promotion is defined by the C++ standard. On the other hand, the conversion of float to Numeric is user-defined. User defined conversions rank lower than standard ones when the compiler has to decide which overloaded version is to be called.
123. Be Cautious with Local Static Variables in a Member Function
The object model of C++ ensures that each object instance gets its own copy of data members (except for static ones). On the other hand, all instances of a base class, as well as instances of classes derived from it, share a single copy of the member functions of the base class. Therefore, declaring a local static variable (not to be confused with static class members) in a member function can cause surprises like this:
class Base {
public: int countCalls() { static int cnt = 0; return ++cnt; } };
class Derived1 : public Base { /*..*/};
class Derived2 : public Base { /*..*/};
Derived1 d1;
int d1Calls = d1.countCalls(); //d1Calls = 1
Derived2 d2;
int d2Calls = d2.countCalls(); //d2Calls = 2 and not 1
The above example may be used to measure load balancing by counting the total number of invocations of the countCalls member function, regardless of the actual object from which it was called. However, obviously that programmer's intention was to count the number of invocations through Derived2 class exclusively. In order to achieve that, a static class member would be preferable:
class Base {
private: static int i; //static class member rather than a local static variable
public: virtual int countCalls() { return ++i; } };
class Derived1 : public Base {
private: static int i; //hides Base::i
public: int countCalls() { return ++i; } //overrides Base:: countCalls() };
class Derived2 : public Base {
private: static int i; //hides Base::i and distinct from Derived1::i
public: virtual int countCalls() { return ++i; } };
Derived1 d1; Derived2 d2;
int d1Calls = d1.countCalls(); //d1Calls = 1
int d2Calls = d2.countCalls(); //d2Calls also = 1
No comments:
Post a Comment