class CBase
{
int i;
public:
void f() { std::cout<<"CBase::f"<<std::endl;}
};
И если где-то дальше вы написали код вроде такого:
CBase* p = NULL;
p->f();
То по стандарту это undefined behavior. Однако очень часто функция отрабатывает нормально. Visual Studio 2008, например, генерит вполне себе работающий код. А чего, вызов этой функции был разрешен на этапе компиляции, указатель this не нужен. И довольно долго может создаваться ощущение, что все нормально работает. Можно даже привести более страшно выглядящий пример
((CBase*)0)->f();
Но на самом деле далеко не все нормально. Как только появляется обращение к this, то программа начинает "падать". Примеры:
class CBase
{
int i;
public:
virtual void f() { std::cout<<"CBase::f"<<std::endl;}
};
class CBase
{
int i;
public:
void f() { std::cout<<"CBase::f"<<std::endl; i=0;}
};
Все это приводит к очень интересным трудно отлавливаемым багам.
Ссылка по теме:
comp.lang.c++.moderated - Functions that don't use this, called with uninitialized pointers
Не совсем понятно, что не так в последнем примере?
ОтветитьУдалить2Yorik.sar:
ОтветитьУдалитьНе совсем понятно, что не так в последнем примере?
Там идет присвоение переменной. int i=0. То есть идет обращение к this.
А разве не происходит сокрытие новой локальное переменной i?
ОтветитьУдалить2Logonoff:
ОтветитьУдалитьА разве не происходит сокрытие новой локальное переменной i?
Угу, моя ошибка. Исправила.
Только не ссылка, а указатель
ОтветитьУдалить2Qehgt:
ОтветитьУдалитьТолько не ссылка, а указатель
Поправила, спасибо.
Был случай, когда из функции, в которой забыли написать return, успешно возвращался осмысленный итератор по вектору. Причем итератор объявлялся в этой функции как локальная переменная.
ОтветитьУдалитьчто мешает сделать метод f статическим?
ОтветитьУдалить2Muxa:
ОтветитьУдалитьчто мешает сделать метод f статическим?
В данном конкретном случае ничего не мешает.
Ага, с этим поведением VC++ доводилось сталкиваться. Он там живет еще с 90-х. Спасало то, что приходилось программу таскать между разными компиляторами. В результате под Watcom-ом программа ломалась там, где под VC++ успешно работала.
ОтветитьУдалитьТогда у меня даже способ ловли особо хитрых багов был такой: если ошибка не желала ловиться под одним компилятором, нужно было попробовать скомпилировать ее другим компилятором :)
Вообще-то undefined на то и undefined, что может произойти все, что угодно. А в таком случае бедный компилятор даже предупреждение выдать не может.
ОтветитьУдалитьНу не так уж и трудно этот баг отлавливается. Единственный дискомфорт: отладчик выдаёт вообще левую строку, но просмотр стека вглубь (хронологии вызова) позволяет легко найти этот косяк.
ОтветитьУдалитьВ gcc это вызовет segmentation fault, по крайней мере на данном примере. Вообще не приятно что нет предупреждений от компилятора, интересно как icc это проглотит.
ОтветитьУдалитьНадо же, сам топтался в прошлом году по этим-же граблям - только с gcc. Программа падала на вполне себе нормальном мьютексе. С тех пор как только начинает происходить что-то загадочное с кодом - сразу смотрю на this.
ОтветитьУдалить2Plambir:
ОтветитьУдалитькомпилятор даже теоретически не может это обнаружить если указатель нулевой из-за провала фабричного метода.
Static code analysis дефект такого плана выловить не может. Точнее, может, но только простые и утрированные примеры.
ОтветитьУдалитьЭту проблему должен был поймать если не сам девелопер, то хотя бы человек, делающий code review. Метод, не обращающийся к data members таекущего объекта должен быть статическим.
Строго говоря, именно такая ситуация особых проблем с диагностикой и отладкой чаще всего не доставляет (если есть навыки отладки и знание инструментальных средств).
ОтветитьУдалитьВот "повисшие" указатели на объекты, которые когда-то были в динамической памяти, могут доставить немало приятных минут...
2evolvah:
ОтветитьУдалитьЭту проблему должен был поймать если не сам девелопер, то хотя бы человек, делающий code review. Метод, не обращающийся к data members таекущего объекта должен быть статическим.
Это в данном синтетическом примере все очевидно. На самом деле все бывает не так. Много кода, его правят несколько человек. Есть if'ы и в какой-то несчастный момент ситуация складывается так, что указатель равен NULL, а if'ами мы все обращения к переменным обошли. После чего добавляем где-то в кода выражение вроде m_i=0; и в ужасе наблюдаем, что после этого программа "падает".
@Alena:
ОтветитьУдалитьДа, но первая версия такой функции не будет содержать обращений к data members, верно? Вот тогда-то и надо сделать ее static. Когда кто-то другой попытается добавить обращение к data member, то получит ошибку от компилятора.
Это вещь того же масштаба, что и передача объектов по const reference, если передаваемый объект не должен быть изменен вызываемой функцией. Эдакая защита от неаккуратного программиста средствами самого языка.
Ошибки времени компиляции обычно на порядки проще и дешевле в устранении, чем ошибки времени выполнения.
Ничего странного в этом нет.
ОтветитьУдалитьКласс в памяти это только его переменные-члены. Функции - это код, который генерится всегда, если к нем есть обращение. (В примере, фактически, статический метод).
Objective-C позволяет слать мессаджы на nil - то есть программа не упадет даже, если в этом методе будет обрашение к переменным класса.
Вы так говорите "программа начнет падать" как будто это что-то плохое.
ОтветитьУдалитьХуже если указатель не 0, а указывает на вполне себе выделенную память, но не принадлежащую объекту этого класса.
Плохо то, что это официально не разрешенное поведение, т.е. каждый компилер как хочет, так и делает. ИМХО стоило бы узаконить путь, как это делает MSVC - самый правильный, this - это просто один из параметров, используй как хочешь.
ОтветитьУдалитьЕсли Watcom генерит неработающий код для этого случая - очень плохо, фактически означает, что есть некая динамическая проверка типа там, где ее быть не должно и соответственно, код неоптимален.
Врядли какая-нибуть проверка, кроме runtime, может это определить, да и runtime в случае С++ часто не будет работать.
2evolvah:
ОтветитьУдалитьВот тогда-то и надо сделать ее static.
Не всегда так получается, особенно если пишется generic код. Часто бывает интересно, например, в дебаге использовать динамические данные с удобным изменением, а в релиз закомилить уже полностью настроенные статические. При этом, чтобы часть данных осталась линамической (простой #ifdef в этом случае не поможет).
Не понимаю, что не так. Ну да, обращения по мусорным указателям иногда дают сегфолт, а иногда нет. Я думал, все привыкли уже. За код типа ((CBase*)0)->f(); надо просто эцих с гвоздями на 72 часа.
ОтветитьУдалитьКонечно, если бы компилятор мог автоматически в каждый метод добавить assert(this) - было бы чуточку приятнее, но мир не идеален :(
2legolegs:
ОтветитьУдалитьЯ думал, все привыкли уже.
Новые программисты прибывают каждый день. И они ни к чему не привыкли, они просто не успели.
Ещё там есть такая подляна, как смещение базы.
ОтветитьУдалитьstruct A { int x; };
struct B {
int y;
void f() {
cout << (void*)this << endl;
if(this==NULL) return;
// безопасно, думаете вы?
y=123;
}
};
struct C : A, B {
void g() { f(); }
};
int main() {
((C*)NULL)->f(); // 1
((C*)NULL)->g(); // 2
}
Нулевые указатели всегда переходят в нулевые указатели, то есть up-casting: (B*)(C*)NULL == (B*)NULL, и наоборот, down-casting: (C*)(B*)NULL == (C*)NULL.
Но это, понятное дело, требует лишней проверки, а в С++ принято не платить за то, что не заказано.
Таким образом, в //1 ещё есть шанс, что компилятор выполнит up-casting указателя перед разыменованием (->). Но в //2, в теле функции C::g(), эти шансы стремительно исчезают.
Смещение базы возникает не только при множественном наследовании.
Например, компиляторы VC и gcc стараются поместить vfptr с нулевым смещением - ценой сдвига базы. (Да, это по Стандарту можно).
struct A {
int x; // чтобы база была не пустой
void f() { cout<<(void*)this; }
};
struct B : A {
virtual void g() {}
};
int main() { ((B*)0)->f(); } // 0x4
Забыл пояснить, в чём фокус со смещением базы.
ОтветитьУдалитьРазница - в обработке пользовательского указателя (который может быть NULL) и this (который, как мы ожидаем, указывает на валидный объект - а следовательно, не должен быть NULL).
Даже пользовательские ссылки - и тех некоторые компиляторы (VC) проверяют на нулёвость (хотя и не обязаны). В отношении же this - или, более общо, левого операнда оператора (->) и (.), такие проверки не производятся.
http://codepad.org/fEhLsZyg
2Kodt:
ОтветитьУдалитьЗабыл пояснить, в чём фокус со смещением базы.
Спасибо за хороший пример.
gcc ведет себя примерно так же. Вообще то что это работает (в смысле вызов метода без обращения к this) оно и понятно, метод класса манглится и линкуется в в сишную функцию и принимает параметр this уже в рантайме. Если сишной функции послать неинициализированный указатель на структуру то она не упадет пока не обратится к ней. Получается странная философия, семантика такой простой вещи как опретора непрямого обращения в случае с p->func(); изменяется так, что очевидные ошибки с ним связанные в Си становятся неочевидными в С++ :)
ОтветитьУдалитьПроблема вовсе не в компиляторе а в программисте.
ОтветитьУдалитьЭто чистой воды статическая функция!
Если вы её не объявили таковой это Ваша ошибка как дизайнера класса.
Это все равно что ругать продавца за то, что кому продали топор. а он отрубил себе пальцы.