I am enrolled in a C course, where i have the following code snippet:
class Pet {
protected:
string name;
public:
Pet(string n)
{
name = n;
}
void run()
{
cout << name << ": I'm running" << endl;
}
};
class Dog : public Pet {
public:
Dog(string n) : Pet(n) {};
void make_sound()
{
cout << name << ": Woof! Woof!" << endl;
}
};
class Cat : public Pet {
public:
Cat(string n) : Pet(n) {};
void make_sound()
{
cout << name << ": Meow! Meow!" << endl;
}
};
int main()
{
Pet *a_pet1 = new Cat("Tom");
Pet *a_pet2 = new Dog("Spike");
a_pet1 -> run();
// 'a_pet1 -> make_sound();' is not allowed here!
a_pet2 -> run();
// 'a_pet2 -> make_sound();' is not allowed here!
}
I'm not able to figure out why this is invalid. Please suggest suitable references for this that have ample explanation about why this is happening.
CodePudding user response:
In C , the types and names of variables at any point is what the compiler permits itself to know.
Each line of code is checked against the types and names of variables in the current scope.
When you have a pointer to a base class, the type of the variable remains pointer to the base class. The actual object it is pointing at could be a derived class, but the variable remains a pointer to the base class.
Pet *a_pet1 = new Cat("Tom");
a_pet1 -> run();
// 'a_pet1 -> make_sound();' is not allowed here!
the type of a_pet1
is Pet*
. It may be pointing at an actual Cat
object, but that is not information that the type of a_pet1
has.
On the next line, you are using a_pet1
. You can only use it in ways that are valid for a Pet
pointer on this line. a_pet1->make_sound()
is not a valid operation on a Pet
pointer, because the Pet
type does not have a make_sound
method.
You could do this:
Cat *a_pet1 = new Cat("Tom");
a_pet1 -> run();
a_pet1 -> make_sound(); // it now works!
because we changed the type of a_pet1
from Pet*
to Cat*
. Now the compiler permits itself to know that a_pet1
is a Cat
, so calling Cat
methods is allowed.
If you don't want to change the type of a_pet1
(which is a reasonable request), that means you want to support make_sound
on a Pet
, you have to add it to the type Pet
:
class Pet {
protected:
string name;
public:
Pet(string n)
{
name = n;
}
void make_sound();
void run()
{
cout << name << ": I'm running" << endl;
}
};
now, a_pet1->make_sound()
will be allowed. It will attempt to call Pet::make_sound
, which is not Dog::make_sound
, and as we didn't provide a definition for Pet::make_sound
, this will result in an error at link time.
If you want Pet::make_sound
to dispatch to its derived methods, you have to tell the compiler this is what you want. C will write the dispatch code for you if you use the virtual
keyword properly, like this:
class Pet {
protected:
string name;
public:
Pet(string n)
{
name = n;
}
virtual void make_sound() = 0;
void run()
{
cout << name << ": I'm running" << endl;
}
};
here I both made make_sound
virtual
, and made it pure virtual. Making it virtual means that the compiler adds information to each Pet
and Pet
derived object so, when it is actually pointing to a derived object type and not a Pet
, the caller can find the right derived method.
Pure virtual (the =0
) simply tells the compiler that the base class method Pet::make_sound
intentionally has no implementation, which also means that nobody is allowed to create a Pet
, or a even Pet
derived object instance, without providing a make_sound
implementation for its actual type.
Finally, note that I mentioned "permits itself to know". The compiler limits what it knows at certain phases of compilation. Your statement that a_pet1
is a Pet*
tells the compiler "I don't want you to assume this is a Cat
, even though I put a Cat
in there". At later stages of compilation, the compiler can remember that fact. Even at runtime, it is sometimes possible to determine the actual type of an object (using RTTI). The forgetting of the type of the object is both intentional and limited.
It turns out that "forced forgetting" is quite useful in a number of software engineering problems.
There are other languages where all method calls to all objects go through a dynamic dispatch system, and you never know if an object can accept a method call except by trying it at runtime. In such a language, calling make_sound
on any object whatsoever would compile, and at runtime it would either fail or not depending on if the object actually has a make_sound
method. C intentionally does not do this. There are ways to gain this capability, but they are relatively esoteric.
CodePudding user response:
In your example a_pet1 and a_pet2 are pointers to objects of the 'Pet' class so your compiler only allows you to access functions that are actually available in that class. The 'Pet' class iteself does not contain a 'make_sound' function in this case. To fix this problem you can define a 'make_sound' function in the base class and mark it as 'virtual'. This will make a function call over a base pointer always invoke the execution of the according function in the inheriting class.
class Pet {
protected:
string name;
public:
Pet(string n)
{
name = n;
}
void run()
{
cout << name << ": I'm running" << endl;
}
virtual void make_sound() {}
};
class Dog : public Pet {
public:
Dog(string n) : Pet(n) {};
void make_sound() override
{
cout << name << ": Woof! Woof!" << endl;
}
};
class Cat : public Pet {
public:
Cat(string n) : Pet(n) {};
void make_sound() override
{
cout << name << ": Meow! Meow!" << endl;
}
};
int main()
{
Pet* a_pet1 = new Cat("Tom");
Pet* a_pet2 = new Dog("Spike");
a_pet1->run();
a_pet1->make_sound();
a_pet2->run();
a_pet2->make_sound();
}