25 декабря 2008

this == NULL. WTF?

Что вы подумаете, если встретите код типа такого:
if (this == NULL)
...;
Нормально ли что this может быть нулевым, ведь при этом даже к членам класса обратиться нельзя, не то что вызвать виртуальную функцию.
Однако тех, кого не удивляет конструкции вида delete this;, не удивит и проверка this на NULL. В самом деле, this - это обычный указатель, и как любой другой указатель он может быть равен NULL:
class foo {
public:
bool am_i_alive() { return this != NULL; }
};

foo *p = NULL;
assert(!p->am_i_alive());

Но не разумнее ли, спрашивается, проверять указатель на NULL "снаружи" класса (в данном примере - проверять p на NULL), а не внутри его. То есть не вызывать методы объекта, если указатель на него нулевой. И так действительно будет разумнее:
void foo(Bar &bar, IFilter *filter = NULL)
{
if (filter)
filter->init(bar);

for (auto b : bar)
if (!filter || filter->is_accepted(b))
out.insert(b);
}
Единственное преимущество внесения проверки внутрь класса - сокращение вызывающего кода, немного повышающее его написание и читаемость. То есть по сути "синтаксический сахар":
void foo(Bar &bar, IFilter *filter = NULL)
{
filter->init(bar);

for (auto b : bar)
if (filter->is_accepted(b))
out.insert(b);
}
Но ценой этого будет то, что вызываемые функции не должны быть виртуальными, то есть придется каждую виртуальную функцию "обернуть" примерно так:

class IFilter
{
public:
void init(Bar& bar) { if (this) init_impl(bar; }
bool is_accepted(Bar::value_type b) { return !this || is_accepted_impl(b); }
private:
virtual void init_impl(Bar&) = 0;
virtual bool is_accepted_impl(Bar::value_type) = 0;
};

Любая книжка по рефакторингу скажет, что добиться подобного кода можно с помощью определеннго паттерна, который предлагает замену нулевых указателей на указатели ненулевые, но указывающие на объект-пустышку:

class IFilter
{
public:
virtual void init(Bar&) = 0;
virtual bool is_accepted(Bar::value_type) = 0;
};

class NullFilter
{
public:
virtual void init(Bar&) {}
virtual bool is_accepted(Bar::value_type) { return true; }
};

void foo(Bar &bar, IFilter &filter = NullFilter())
{
filter.init(bar);

for (auto b : bar)
if (filter.is_accepted(b))
out.insert(b);
}


В качестве члена какого-нибудь класса это может выглядеть так:
class Foo
{
public:
Foo() : filter(new NullFilter) {}
void set_filter(auto_ptr<IFilter> f) { filter = f; }
void use_filter()
{
// можно смело использовать конструкции filter.get()->xxx()
}
private:
auto_ptr<IFilter> filter;
};
Единственное, что может заставить не послушаться книжку по рефакторингу, так это профайлер ;)

11 комментариев:

Maccer комментирует...

Как работает for (auto b : bar) ?

Raider комментирует...

for, auto

IUnknown комментирует...

А что уже существуют компиляторы в которых реализованы новинки C++0x?!

Raider комментирует...

IUnknown: ну ты же понимаешь, что в данном случае эта конструкция была использована просто в качестве примера. Можно было бы написать просто "for (...)".

ps. А компиляторы уже начинают поддерживать C++0x: gcc, VC++.

Анонимный комментирует...

void foo(Bar &bar, IFilter &filter = NullFilter())
разве такая запись без модификатора const допустима?

Raider комментирует...

Аноним: не знаю как по стандарту, но мой компилятор (VS8) компилирует такое даже без варнингов:

struct x { x() {} };
void z(x &y = x()) { }

Анонимный комментирует...

да, да - всё верно, я стормозил
тут временная переменная создаётся в контексте вызывающего функцию, а не в самой функции - поэтому всё ok

Анонимный комментирует...

"Но ценой этого будет то, что вызываемые функции не должны быть виртуальными".. Не понял, поясни пожалуйста.

Raider комментирует...

Когда функция не виртуальная, ее адрес известен на этапе компиляции. Если же она виртуальная, то ее адрес берется из таблицы виртуальных функций. Эта таблица хранится рядом с объектом. То есть она хранится где-то там, куда указывает this. Но если this равен NULL, то где взять виртуальную таблицу? ;) То есть виртуальную функцию не вызовешь с this==NULL.

Анонимный комментирует...

А почему бы не руководствоваться правилом "использовать ссылки вместо указателей", тогда отсюда будет правило "проверить указатель на NULL" ПЕРЕД его разыменованием. Все ваши "костыли" излишни и не улучшают код ни на сколько.

Анонимный комментирует...

У Вас изначально неверные посылки:
1) Когда вы делаете delete this, this НЕ становится равным NULL.
2) this вообще-то является const указателем, поэтому присвоить ему NULL нельзя.
P.S. WTL и MFC являются плохими примерами библиотек. Тот код, которые они демонстрируют, не должен служить примером для подражания. Посмотрите в сторону wxWidgets. А если решите писать свою библиотеку посмотрите на SmartWin.