Pull to refresh

Comments 18

Они и сами путаются и стандарт всё дальше запутывают. Функция вызова невиртуального метода для указателя null нужна для реализации проверок в операторах преобразования типа, перегруженных операторов (при работе с коллекциями, например) и т.п. При нынешних формулировках этого сделать нельзя. Но можно при этом можно извратиться и получить похожий результат с дружественными функциями. Почему — загадка великая есмь.
При том, что вызов невиртуального метода для NULL — совершенно нормальная с точки зрения компилятора вещь без всякого криминала — ну будет неявный параметр (this) равен NULL и что? Код внутри метода может корректно обрабатывать такие ситуации.
При том, что вызов невиртуального метода для NULL — совершенно нормальная с точки зрения компилятора вещь без всякого криминала — ну будет неявный параметр (this) равен NULL и что? Код внутри метода может корректно обрабатывать такие ситуации.


Вызов метода от nullptr это UB. Точнее, UB — это проверять this на nullptr. В теории, можно написать метод, который this не проверяет и не использует, как в примере в статье — и это будет работать. Но зачем вам метод класса который не использует this, сделайте его static.
Теперь, собственно, почему проверка this это UB. Рассмотрим пример
struct A 
{
    static inline int globalInt = 0;
    int localInt = 0;
    int foo() { if (!this) return globalInt; else return localInt; }
    // сеттер не нужен для понимания идеи но пусть будет для полноты
    void setFoo(int i) { if (!this) globalInt = i; else localInt = i; }
};

вот так всё работает
A *a = nullptr;
int i = a->foo();


Усложним пример, добавим немного наследования
class B { int i{0}; int j{0}; };
class С : public B, public A {};


И попробуем сделать тоже самое
C *c = nullptr;
int i = c->foo();

Упс, теперь this для базового класса A — это nullptr + sizeof(B). Немного не то, что ожидалось
Вызов метода от nullptr это UB. Точнее, UB — это проверять this на nullptr.

По стандарту, вызов метода от nullptr — это UB, а сравнение this с nullptr легально, всегда возвращает false, и на него уже давно ругаются компиляторы как на бессмысленный код.
Да, я неправильно выразился, должно быть «Вызов метода от nullptr это UB. А также, UB — это проверять this на nullptr.» Никогда особо не умел связно писать тексты=(
На сколько я знаю, не возвращают они false всегда, т.к. я видел всякий legacy-код в значимых количествах, который допускает вызов методов с this=nullptr. Ругаются ворнингами — да, но сравнение отрабатывают честно, т.к. иначе ломается legacy.
Потому и возмущаюсь, что формально формулировки новых редакций стандарта ломают legacy на ровном месте, а уже разработчикам компиляторов приходится костыли подставлять. И то же время в стандарте разводят реальный ад в попытках сохранить легаси там, где его можно было смело поломать, а 0.00001% поломанного некорректного кода заставить добавив какую-нибудь прагму (привет безумию с конструкторами, где половина безумия не нужна, а вторая половина ещё и умудряется ломать старый код).
Я такое только у MFC помню, и кажется MS специально поддерживала this=0 в компиляторе, чтобы старый код работал. Я не знаю, поддерживается ли это до сих пор последними версиями студии, но gcc точно убирает все проверки `if (this)` начиная еще с 6-й версии (хотел написать «недавно начала», но потом понял, что уже несколько лет прошло с даты релиза).
Нет. Только что проверил — «if (this==nullptr)» отрабатывает корректно в GCC 7.1.1
Уверен, что и в девятой будет работать. Там реально есть популярный легаси код, который с этим работает.
Или это баг в оптимизаторе компилятора или я чего-то не понимаю.
На GCC 7.1.1 вот это работает правильно:
#include <iostream>

using namespace std;

class A {
public:
	void non_virtual_mem_fn() {
            if (this==nullptr)
                cout << "NULL" << endl; 
            else
                cout << "NOT NULL" << endl; 
    }
};

int main() {
    A* ptr_null = nullptr;
    ptr_null->non_virtual_mem_fn();
    A* ptr_not_null = new A();
    ptr_not_null->non_virtual_mem_fn();
    delete(ptr_not_null);
    return 0;
}

Не баг, а неожиданная фича ;-)
Ага, опция -O1/-O2 меняет поведение компилятора на противоположное.
Проверил на godbolt на минимальном примере, проверка выполняется только при отключенной оптимизации (-O0):
godbolt.org/z/4qdbzq
Чудеса — мой код, который выше там же работает при "-O1". Прямо, эталонное UB :)
При линейном наследовании объекты «растут» в сторону увеличения адресов, и указатель на объект всегда указывает на самого первого предка, а потомки «знают», что их данные располагаются в памяти после данных предка.
И, да, только в случае множественного наследования может быть преобразование указателя с интересными результатами.
Но множественное наследование и виртуальные функции это уже частные случаи, где UB действительно неизбежно. Зачем было распространять его на линейное наследование и невиртуальные функции?
И именно за такой идиотизм с UB на ровном месте авторам стандарта и нужно гореть в аду.
Чем дальше в лес, тем меньше света…
Диву даёшься, как чтение стандарта C++ всё дальше превращается в гуманитарную дисциплину с анализом значений синонимов и мнений авторитетных толкователей.
Скажите ещё спасибо, что пока гадания не завезли. Ну или в сонники не добавили «что делать, если приснился нулевой указатель».
Sign up to leave a comment.

Articles