
Search for uninitialized class members

We have been contacted by clients and potential clients for a long time with a request to implement diagnostics to search for uninitialized class members. We resisted for a long time, realizing the complexity of the task, but still surrendered. As a result, we created the V730 diagnostics . The diagnostics turned out to be far from ideal, and anticipating many letters that something was not working correctly, I decided to write a note on the technical complexity of this task. I think this information will give PVS-Studio users answers to some questions in advance and will simply be of interest to a wide circle of our readers.
Speaking about the search for uninitialized members of a class, a person imagines rather simple situations. There are 3 members in the class. We initialized two of them, and forgot one. Well, something like that:
class Vector
{
public:
int x, y, z;
Vector() { x = 0; y = 0; }
};
Oh, if the world were so simple and everyone used only such classes. In practice, it is sometimes difficult even for a person to say whether the code contains an error or not. In the case of the analyzer, sometimes this problem is not solved at all.
Let's look at the various reasons why the analyzer may give false warnings or, conversely, not notice errors.
To begin with, I note that class members can be initialized in a variety of ways. Everything is even difficult to mention. Do not read further. Take a look at the picture for now and try to list how many member initialization methods you know. Have you counted? Then let's continue.

Figure 1. The unicorn wonders if a class member is initialized or not.
About some initialization methods:
- Just assign a value to the class member: A () {x = 1; }.
- Use initialization list: A (): x (1) {}
- Use access through this: A (int x) {this-> x = x; }
- Use access through "::": A (int x) {A :: x = x; }
- Use C ++ 11-style initialization: class A {int x = 1; int y {2}; ...};
- Initialize the field using functions like memset (): A () {memset (& x, 0, sizeof (x);}.
- Initialize with memset () all the fields of the class at once (yes, yes, they do): A () {memset (this, 0, sizeof (* this)); }
- Use the delegating constructor (C ++ 11): A (): A (10, 20) {}
- Use a special initialization function: A () {Init (); }
- Class members can initialize themselves: class A {std :: string m_s; ...};
- Class members can be static.
- You can initialize the class by explicitly calling another constructor: A () {this-> A (0); }
- You can call another constructor using placement new (programmers are such inventors): A () {new (this) A (1,2); }
- You can initialize members indirectly through a pointer: A () {int * p = & x; * p = 1; }
- And through the link: A () {int & r = x; r is 1; }
- You can initialize members if they are classes by calling their functions: A () {member.Init (1, 2); }
- You can "gradually" initialize members that are structures: A () {m_point.x = 0; m_point.y = 1; }
- There are other ways.
Of particular complexity are calls to initialization functions, which in turn call other functions, and so on. Tracking this chain of calls can be extremely difficult, and sometimes even impossible.
However, even if you learn to recognize all-all methods of initializing classes, this will still not be enough. Failure to initialize a member is not always a mistake. The classic case is the implementation of a kind of container. Often you can find a similar code:
class MyVector
{
size_t m_count;
float *m_array;
public:
MyVector() : m_count(0) { }
....
};
The m_array variable is not initialized, but it does not matter. At first, the class stores 0 elements, and therefore no memory is allocated for the array. Accordingly, the m_array pointer is not initialized. It will be initialized later when at least one element is added to the container.
The code is correct, but the analyzer will give a false warning, which will upset the programmer. But what to do with such cases is unclear.
Perhaps for reliability, you should initialize m_array with nullptr. However, a discussion of programming style is beyond the scope of this article. It is important that in practice, if not all members are initialized in the constructor, this does not mean anything. The code can work correctly and not initialize something for the time being quite reasonable. Here I showed a very simplified example. There are much more difficult situations.
And now about the duality of the world. Look at the abstract code:
class X
{
....
char x[n];
X() { x[0] = 0; }
....
};
Error that in class X only 1 element is initialized? It is impossible to give an answer. It all depends on what class X is. And the analyzer cannot understand this. For this you need a man.
If this is some kind of string class, then there is no error:
class MyString
{
....
char m_str[100];
MyString() { m_str[0] = 0; }
....
};
A terminal zero is written to the beginning of the line . Thus, the programmer indicates that the line is empty. The remaining elements of the array can not be initialized. The code is correct.
If this is a class for working with color, then an error occurs:
class Color
{
....
char m_rgba[4];
Color() { m_rgba[0] = 0; }
....
};
Here, only one element of the array was initialized, and all elements should be initialized. In this case, by the way, the analyzer will consider that the class is fully initialized and will not generate a warning (false negative). We have to give preference to the “keep silent” approach, otherwise the analyzer will generate too much noise.
See how complicated and ambiguous it is? It is very difficult to say when there is a mistake, and when not. I had to implement a lot of empirical tests in the analyzer that try to guess if the code is wrong or not. Naturally, the empirical approach will fail, and we want to apologize in advance for this. However, I hope that now you understand that finding uninitialized members of a class is a difficult task and you will be indulgent to PVS-Studio.