Анализатор обнаружил опасные приведения указателя "this" к типу "void*" и последующее обратное приведение "void*" к типу класса. Само по себе преобразование "this" к типу "void*" не является ошибкой, но в ряде случаев ошибочным является обратное преобразование - от "void*" к типу указателя на класс. В результате таких преобразований возможно получение некорректного указателя.
Описание диагностики достаточно большое и сложное, но, к сожалению, сделать его проще не получается. Просим внимательно прочитать его целиком.
Рассмотрим пример, где используется приведение "this" к "void*", а после - некорректное обратное приведение к типу класса:
class A
{
public:
A() : firstPart(1){}
void printFirstPart() { std::cout << firstPart << " "; }
private:
int firstPart;
};
class B
{
public:
B() : secondPart(2){}
void* GetAddr() const { return (void*)this; }
void printSecondPart() { std::cout << secondPart << " "; }
private:
int secondPart;
};
class C: public A, public B
{
public:
C() : A(), B(), thirdPart(3){}
void printThirdPart() { std::cout << thirdPart << " "; }
private:
int thirdPart;
};
void func()
{
C someObject;
someObject.printFirstPart();
someObject.printSecondPart();
someObject.printThirdPart();
void *pointerToObject = someObject.GetAddr();
....
auto pointerC = static_cast<C*>(pointerToObject);
pointerC->printFirstPart();
pointerC->printSecondPart();
pointerC->printThirdPart();
}
Можно было бы предположить, что вывод будет следующим:
1 2 3 1 2 3
Однако на самом деле на экране отобразится что-то типа:
1 2 3 2 3 -858993460
В итоге вывод для всех данных после преобразований является некорректным. Проблема кроется в том, что теперь указатель "pointerC" указывает не на начало объекта C, а на блок памяти, выделенной под объект B.
Кажется, что такая ошибка надуманна и допустить её невозможно. Однако ошибка очевидна только из-за того, что пример маленький и простой. В настоящих программах со сложными иерархиями классов можно легко запутаться. Особенно коварно то, что если функцию "GetAddr()" расположить в классе A, то всё работает, а если в классе B, то нет. Это может сбивать с толку. Давайте разберемся в ситуации подробнее.
Чтобы было проще понять, из-за чего возникла ошибка, необходимо знать, как конструируются и располагаются в памяти объекты классов, созданных в результате множественного наследования.
Схематичный пример этого изображён на рисунке 1.
Рисунок 1 - Расположение в памяти объекта класса, полученного путём множественного наследования
Из рисунка 1 видно, что объект класса С (который и получен в результате множественного наследования) состоит из объектов классов A и B, плюс часть объекта C.
Указатель "this" содержит в себе адрес начала выделенного под объект блока памяти. На рисунке 2 изображены указатели "this" для всех трёх объектов.
Рисунок 2 - Указатели "this", и блоки памяти
Так как объект класса C состоит из трёх частей, указатель "this" для него будет указывать не на блок памяти, который добавлен дополнительно к базовым классам, а на начало всего непрерывного блока памяти. То есть в данном случае, указатели "this" для классов A и C совпадут.
Указатель "this" для объекта класса B указывает на начало выделенного под него блока памяти, но при этом адрес начала этого участка памяти будет отличен от адреса начала участка памяти, выделенной под объект класса C.
Таким образом, при вызове метода "GetAddr()", мы получим адрес объекта B, и в результате обратного преобразования полученного указателя к типу "C*" будет получен некорректный указатель.
Т.е. если бы функция "GetAddr()" располагалась в классе A, то всё бы работало так, как и ожидал программист. Но если она расположена в B, то происходит сбой.
Во избежание подобных ошибок необходимо продумать, действительно ли стоит выполнять приведение "this" к "void*", и если всё же стоит - тщательно проверять иерархию наследования, а также дальнейшие операции обратного преобразования от "void*" к типу указателя на класс.
Дополнительные ссылки:
Данная диагностика классифицируется как: