📜 ⬆️ ⬇️

Search for uninitialized class members

Unicorn and class members
For a long time, we were approached by customers and potential customers 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 diagnostic. Diagnostics turned out far from ideal and anticipating a lot of letters that something is wrong, I decided to write a note about the technical complexity of this task. I think this information in advance will give the PVS-Studio users answers to some questions and will simply be interesting to a wide circle of our readers.

Speaking about the search for uninitialized members of a class, a person imagines quite simple situations. There are in the class of say 3 members. We initialized two of them and forgot one. Well, something in this spirit:
class Vector { public: int x, y, z; Vector() { x = 0; y = 0; } }; 

Eh, if the world was 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 various reasons why the analyzer may issue false warnings or, on the contrary, overlook errors.

For a start, I note that class members can be initialized in a variety of ways. Everything is even difficult to remember. Do not read further. Look at the picture for now and try to list how many member initialization methods you know. Counted? Then we continue.
')
Figure 1. A unicorn wonders if a member is initialized or not.
Figure 1. A unicorn wonders if a member is initialized or not.

About some methods of initialization:
  1. Just assign a value to a class member: A () {x = 1; }.
  2. Use initialization list: A (): x (1) {}
  3. Use access through this: A (int x) {this-> x = x; }
  4. Use access through "::": A (int x) {A :: x = x; }
  5. Use initialization in the spirit of C ++ 11: class A {int x = 1; int y {2}; ...};
  6. Initialize the field using memset () functions: A () {memset (& x, 0, sizeof (x);}.
  7. Initialize with the help of memset () all the fields of the class at once (yes, yes, they do this): A () {memset (this, 0, sizeof (* this)); }
  8. Use delegating constructor (C ++ 11): A (): A (10, 20) {}
  9. Use a special initialization function: A () {Init (); }
  10. Class members can initialize themselves: class A {std :: string m_s; ...};
  11. Class members may be static.
  12. You can initialize the class by explicitly calling another constructor: A () {this-> A (0); }
  13. You can call another constructor using placement new (programmers are such inventors): A () {new (this) A (1,2); }
  14. You can initialize members indirectly through a pointer: A () {int * p = & x; * p = 1; }
  15. And through the link: A () {int & r = x; r = 1; }
  16. You can initialize members if they are classes by calling their functions: A () {member.Init (1, 2); }
  17. You can "gradually" initialize members that are structures: A () {m_point.x = 0; m_point.y = 1; }
  18. There are other ways.
As you can see, there are a lot of ways to be taken of how class members are initialized. I think we still know far from everything. But these diverse situations must be foreseen and taken into account!

Of particular difficulty are calls to initialization functions, which in turn call other functions, and so on. Tracing this call chain is extremely difficult, and sometimes impossible at all.

However, even if you learn to recognize all-all methods of class initialization, this will still be not enough. The lack of initialization of a member is not always an error. The classic case is the implementation of a variation of some kind of container. You can often find similar code:
 class MyVector { size_t m_count; float *m_array; public: MyVector() : m_count(0) { } .... }; 

The variable m_array is not initialized, but it does not matter. 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 that will upset the programmer. But how to deal with such cases is not clear.

Perhaps for reliability, m_array should be initialized to nullptr. However, the discussion of programming style is beyond the scope of the note. It is important that in practice, if not all members are initialized in the constructor, this does not mean anything. The code can work perfectly correctly and not initialize something for the time being is quite reasonable. Here I set 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 answer. It all depends on what the class X is. And the analyzer cannot understand this. For this you need a man.

If this is some class of the string, then there is no error:
 class MyString { .... char m_str[100]; MyString() { m_str[0] = 0; } .... }; 

At the beginning of the line is written terminal zero . Thus, the programmer indicates that the string 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 have been initialized. In this case, by the way, the analyzer will consider that the class is fully initialized and will not issue a warning (false negative). One has to give preference to the “silence” approach, otherwise the analyzer will generate too much noise.

See how everything is not easy and ambiguous? It is very difficult to say when there is an error, and when not. I had to implement in the analyzer a lot of empirical checks that try to guess if the code is erroneous or not. Naturally, the empirical approach will fail, and we want to apologize for this in advance. However, I hope now you understand that the search for uninitialized members of a class is a difficult task and will be lenient with PVS-Studio.

Source: https://habr.com/ru/post/269599/


All Articles