In different programming languages, the behavior of virtual functions differs when it comes to constructors and destructors. Incorrect use of virtual functions is a classic mistake. Developers often use virtual functions incorrectly. In this article, we discuss this classic mistake.
I suppose the reader is familiar with virtual functions in C++. Let's get straight to the point. When we call a virtual function in a constructor, the function is overridden only within a base class or a currently created class. Constructors in the derived classes have not yet been called. Therefore, the virtual functions implemented in them will not be called.
Let me illustrate this.
Explanations:
Let's create an object of the C class and call these two functions in the class B constructor. What would happen?
Now look at the same thing in the code.
#include <iostream>
class A
{
public:
A() { std::cout << "A()\n"; };
virtual void foo() { std::cout << "A::foo()\n"; };
virtual void bar() { std::cout << "A::bar()\n"; };
};
class B : public A
{
public:
B() {
std::cout << "B()\n";
foo();
bar();
};
void bar() { std::cout << "B::bar()\n"; };
};
class C : public B
{
public:
C() { std::cout << "C()\n"; };
void foo() { std::cout << "C::foo()\n"; };
void bar() { std::cout << "C::bar()\n"; };
};
int main()
{
C x;
return 0;
}
If we compile and run the code, it outputs the following:
A()
B()
A::foo()
B::bar()
C()
The same happens when we call virtual methods in destructors.
So, what's the problem? You can find this information in any C++ programming book.
The problem is that it's easy to forget about it! Thus, some programmers assume that foo and bar functions are called from the most derived C class.
People keep asking the same question on forums: "Why does the code run in an unexpected way?" Example: Calling virtual functions inside constructors.
I think now you understand why it's easy to make a mistake in such code. Especially if you write code in other languages where the behavior is different. Let's look at the code fragment in C#:
class Program
{
class Base
{
public Base()
{
Test();
}
protected virtual void Test()
{
Console.WriteLine("From base");
}
}
class Derived : Base
{
protected override void Test()
{
Console.WriteLine("From derived");
}
}
static void Main(string[] args)
{
var obj = new Derived();
}
}
If we run it, the program outputs the following:
From derived
The corresponding visual diagram:
The function overridden in the derived class is called from the base class constructor!
When the virtual method is called from the constructor, the run-time type of the created instance is taken into account. The virtual call is based on this type. The method is called in the base type constructor. Despite this, the actual type of the created instance — Derived. This determines the choice of the method. You can read more about virtual methods in the specification.
Note that this behavior can cause errors. For example, if a virtual method works with members of a derived type that have not yet been initialized in its constructor. In this case, there would be problems.
Look at the example:
class Base
{
public Base()
{
Test();
}
protected virtual void Test() { }
}
class Derived : Base
{
public String MyStr { get; set; }
public Derived(String myStr)
{
MyStr = myStr;
}
protected override void Test()
=> Console.WriteLine($"Length of {nameof(MyStr)}: {MyStr.Length}");
}
If we try to create an instance of the Derived type, NullReferenceException is thrown. That happens even if we pass a value other than null as an argument: new Derived("Hello there").
The constructor of the Base type calls an instance of the Test method from the Derived type. This method accesses the MyStr property. It is currently initialized with a default value (null) and not the parameter passed to the constructor (myStr).
Done with the theory. Now let me tell you why I decided to write this article.
It all started with a question on Stack Overflow: "Scan-Build for clang-13 not showing errors". More precisely, it all started with a discussion in comments under our article — "How we sympathize with a question on Stack Overflow but keep silent".
You don't have to follow the links. Let me briefly retell the story.
One person asked how static analysis helps to look for two patterns. The first pattern relates to variables of the bool type. We don't discuss it in this article, so we are not interested in this pattern now. The second one is about searching for virtual function calls in constructors and destructors.
Basically, the task is to identify virtual function calls in the following code fragment:
class M {
public:
virtual int GetAge(){ return 0; }
};
class P : public M {
public:
virtual int GetAge() { return 1; }
P() { GetAge(); } // maybe warn
~P() { GetAge(); } // maybe warn
};
Suddenly, it turns out that not everyone understands the danger here and why static analysis tools warn developers about calling virtual methods in constructors/destructors.
The article on habr has the following comments (RU):
Abridged comment N1: So the compiler's right, no error here. The error is only in the developer's logic. This code fragment always returns 1 in the first case. He could use inline to speed up the constructor and the destructor. It doesn't matter to the compiler anyway. The result of the function is never used, the function doesn't use any external arguments — the compiler will just throw an example as an optimization. This is the right thing to do. As a result, no error here.
Abridged comment N2: I didn't get the joke about virtual functions at all. [quote from a book about virtual functions]. The author emphasizes that the keyword virtual is used only once. The book further explains that it is inherited. Now, my dear students, answer me: what's wrong with calling a virtual function in the class constructor and destructor? Describe each case separately. I assume you're both far from being diligent students. You have no idea when the class constructor and destructor are called. Besides, you missed the lesson "In what order to determine objects of parent classes when you determine a parent, and in what order to destroy them".
After reading the comments, you're probably wondering how they relate to the topic discussed later. And you have every right to do so. The answer is that they don't.
The person who left these comments couldn't guess what kind of problem the author of the question on Stack Overflow wanted to protect the code from.
I admit that the author could have framed the question better. Actually, the code above has no problems. Yet. But they will appear later, when classes obtain new children that implement the GetAge function. If this code fragment had another class that inherit P, the question would become more complete.
However, anyone who knows the C++ language well immediately understands the problem and why this person is so concerned about function calls.
Even the coding standards prohibit virtual function calls in constructors/destructors. For example, the SEI CERT C++ Coding Standard has the following rule: OOP50-CPP. Do not invoke virtual functions from constructors or destructors. Many code analyzers implement this diagnostic rule. For example, Parasoft C/C++test, Polyspace Bug Finder, PRQA QA-C++, SonarQube C/C++ Plugin. PVS-Studio (static analysis tool developed by us) implements it too — the V1053 diagnostic.
We have not studied such a situation. That is, everything works as we expected. In this case, we can explicitly specify which functions we plan to call:
B() {
std::cout << "B()\n";
A::foo();
B::bar();
};
Thus, your teammates will correctly understand the code. Static analyzers will also understand the code and remain silent.
Static analysis is helpful. It identifies potential problems in code. Even those that you and your teammates could've missed. A couple of examples:
The way virtual functions work is not such secret knowledge as the examples above :). However, the comments and questions on Stack Overflow show that this topic deserves attention and control. If it was obvious, I wouldn't write this article. Static analyzers help developers work with code.
Thank you for your attention, come and try the PVS-Studio analyzer.