We all know that in C ++ there is no such thing as a
virtual constructor that would collect the object we need depending on any input parameters at runtime. Typically, for these purposes, a parameterized
factory method (Factory Method) is used . However, we can make a “knight's move” and mimic the behavior of a virtual constructor using a technique called “envelope and letter” (“Letter / Envelope”).
I don’t remember where I found out about it, but, if I’m not mistaken, Jim Coplien (aka James O. Coplien) suggested such a technique in the book
Advanced C ++ Programming Styles and Idioms .
')
The idea is to store a pointer to an object of the same type (Letters) inside the base class (Envelope). At the same time, the Envelope should “redirect” calls of virtual methods to the Letter. With good examples I have, as always, small problems, so we “model” a system of magic techniques (or spells) =) Suppose that for each technique one of the five main elements is used (or maybe their combination), on which the effect of this technology on the world and on the subject to which it applies. At the same time, we want to be able to work with all technicians regardless of their type.
To do this, create a base class Skill and its derivatives for each of the types of techniques. In the base class, purely for example, we define three virtual methods for creating, displaying equipment, and also for clearing memory after it.
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <vector>
using std :: cout ;
using std :: endl ;
enum
{
FIRE = 0x01 ,
WIND = 0x02 ,
LIGHTNING = 0x04 ,
SOIL = 0x08 ,
WATER = 0x10
} ;
class Skill // aka Jutsu =)
{
public :
// virtual (envelope) constructor (see below)
Skill ( int _type ) throw ( std :: logic_error ) ;
// destructor
virtual ~Skill ( )
{
if ( mLetter )
{
// virtual call in destructor!
erase ( ) ;
}
delete mLetter ; // delete Letter for Envelope
// delete 0 for Letter
}
virtual void cast ( ) const { mLetter - > cast ( ) ; }
virtual void show ( ) const { mLetter - > show ( ) ; }
virtual void erase ( ) { mLetter - > erase ( ) ; }
protected :
// letter constructor
Skill ( ) : mLetter ( NULL ) { }
private :
Skill ( const Skill & ) ;
Skill & operator = ( Skill & ) ;
Skill * mLetter ; // pointer to letter
} ;
class FireSkill : public Skill
{
public :
~FireSkill ( ) { cout << "~FireSkill()" << endl ; }
virtual void cast ( ) const { cout << "Katon!" << endl ; }
virtual void show ( ) const { cout << "FireSkill::show()" << endl ; }
virtual void erase ( ) { cout << "FireSkill:erase()" << endl ; }
private :
friend class Skill ;
FireSkill ( ) { }
FireSkill ( const FireSkill & ) ;
FireSkill & operator = ( FireSkill & ) ;
} ;
class WoodSkill : public Skill
{
public :
~WoodSkill ( ) { cout << "~WoodSkill()" << endl ; }
virtual void cast ( ) const { cout << "Mokuton!" << endl ; }
virtual void show ( ) const { cout << "WoodSkill::show()" << endl ; }
virtual void erase ( ) { cout << "WoodSkill::erase()" << endl ; }
private :
friend class Skill ;
WoodSkill ( ) { }
WoodSkill ( const WoodSkill & ) ;
WoodSkill & operator = ( WoodSkill & ) ;
} ;
Skill :: Skill ( int _type ) throw ( std :: logic_error )
{
switch ( _type )
{
case FIRE :
mLetter = new FireSkill ;
break ;
case SOIL | WATER :
mLetter = new WoodSkill ;
break ;
// ...
default :
throw std :: logic_error ( "Incorrect type of element" ) ;
}
// virtual call in constructor!
cast ( ) ;
}
int main ( )
{
std :: vector < Skill * > skills ;
try
{
skills. push_back ( new Skill ( FIRE ) ) ;
skills. push_back ( new Skill ( SOIL | WATER ) ) ;
// skills.push_back(new Skill(LIGHTNING));
}
catch ( std :: logic_error le )
{
std :: cerr << le. what ( ) << endl ;
return EXIT_FAILURE ;
}
for ( size_t i = 0 ; i < skills. size ( ) ; i ++ )
{
skills [ i ] - > show ( ) ;
delete skills [ i ] ;
}
return EXIT_SUCCESS ;
}
In principle, this is not so interesting, but the output will be as follows:
Katon!
Mokuton!
FireSkill :: show ()
FireSkill: erase ()
~ FireSkill ()
WoodSkill :: show ()
WoodSkill :: erase ()
~ WoodSkill ()
Let's better understand what is happening.
So, we have a class Skill (envelope) containing a pointer to an object of the same type (letter). The copy constructor and the assignment operator are hidden in private out of harm's way. The main interest is represented by two constructors of the class, one of which is open and the other protected, as well as the destructor.
An open constructor, also known as the Envelope constructor, or, in our case, the “virtual constructor” (its definition is below), takes one parameter - the “element” type, on the basis of which the type of the constructed object will be calculated. Depending on the input parameter, the pointer to the letter is initialized with a pointer to a specific object (FireSkill, WoodSkill, etc., which are inherited from Skill). If the input parameter contains an invalid value, an exception is thrown.
In derived classes, the FireSkill, WoodSkill, etc. technician. The constructors are closed by default, but the base class Skill is declared as friend, which allows you to create objects of these classes only inside the class Skill. The copy constructor and the assignment operator in these classes are closed and undefined. All virtual methods of the class Skill are redefined in derivatives.
A virtual constructor must be defined below all derived classes so that it does not have to be soared with forward declarations, since objects of derived classes are created inside it.
When objects of derived classes are created in the virtual constructor, when constructing these objects, the default constructor of the base class must first be called. The default base class constructor does nothing except for initializing the pointer to the letter with zero. In fact, this constructor is a letter constructor, which we specify when writing zero to mLetter.
How do you invoke virtual methods? In the base class, inside the virtual methods, there is a “redirection”: in fact, the Envelope plays the role of a shell, which simply calls the Letter methods. Since the Letters methods are invoked via a pointer, late binding occurs, that is, the call will be virtual. Moreover! We can virtually call methods in the constructor and
destructor : when creating a Skill object (Envelope), the parameterized constructor of this class is called, which constructs the Letter and initializes the mLetter. After that we call cast (), inside of which is the mLetter-> cast () call. Since the mLetter is already initialized at this point, a virtual call occurs.
The same in ~ Skill () destructor. First we check if mLetter is initialized. If yes, then we are in the destructor of the Envelope, so we virtually call the method of stripping the Envelope, and then delete it. If not, then we are in the destructor of the Envelope, in which delete 0 is executed (and this construction is completely safe).
Important points:- All objects are now created through a single constructor, and then we seem to work with the base class object. All virtual calls are inside the class itself. We can even create an object of the class Skill in the stack - the methods of this object will still work as if they were virtual.
- In the constructor and destructor, we can use a virtual method call.
- The base class is, one might say, in some way abstract, because all its virtual methods must be redefined in derived classes. If you do not do this, it will lead to the fact that, for example, mLetter-> cast () will be nothing more than an attempt to call the NULL-pointer method.
- When the virtual constructor is called, the type of the object being created will be determined at the execution stage and not at the compilation stage. However, such a call should be enclosed in a try-catch block, otherwise you can skip the exception.
- If we want to add another virtual method to the base class, we will have to override it in all derivatives.
I hope someone will come in handy;)
FIN.
