this->Func() и Func(this)
От: Андрей Тарасевич Беларусь  
Дата: 28.04.02 17:45
Оценка: 47 (7)
#Имя: FAQ.cpp.functhis
Здравствуйте VVV, Вы писали:

VVV>Если посмотреть с другой стороны, то любой вызов C++ функции можно описать в терминам C функции, т.е. это будет функция, где первым параметром передаётся указатель на какую-то структуру данных (this). Посему, при вызове такой функции из C этот параметр (this) можно задать каким угодно (и NULL в том числе).


В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.

Предположим, что у нас имеется вот такой класс A

struct A 
{ 
  void foo(int i) { cout << (this != NULL ? "not NULL" : "NULL") << " " << i << endl; }
};


Перепишем метод 'A::foo' в виде "эквивалентной" С функции, как ты предлагаешь (если я тебя правильно понял):

void foo(A* this_, int i) { cout << (this_ != NULL ? "not NULL" : "NULL") << " " << i << endl; }


Пусть от класса A и класса B множественно унаследован класс C:

struct B { int x; };

struct C : B, A {};


Рассмотрим вот такой вызов метода 'foo' через null-указатель типа 'C*':

C* p = NULL;

p->foo(5); // (1)


Как будет выглядеть соответствующий вызов С функции 'foo'? Он будет выглядеть вот так:

foo(p, 5); // (2)


Эквивалентны ли эти вызовы (закроем глаза на неопределенное поведение в вызове (1))? Нет, не эквивалентны. При компиляции, например, MSVC++, эти вызовы распечатают разные результаты на экране. Почему так происходит?

Происходит это потому, что в вызове (2) компилятор вынужден сделать стандартную pointer conversion — конверсию типа первого параметра от типа 'C*' к типу 'A*'. При этом, согласно спецификации языка С++, null-указатель одного типа должен быть переведен в null-указатель другого типа. По этой причине в результате вызова (2) внутрь функции 'foo' будет передан null-указатель и неравенство 'this_ != NULL' не выполнится.

А в вызове (1) ситуация совершенно иная. Для того, чтобы взывать метод класса A через указатель типа 'C*', этот указатель тоже необходимо определенным образом "преобразовать". То, что происходит с указателем в таком случае, не является никакой "официальной" конверсией языка С++. Компилятор обязан просто как-то "сдвинуть" указатель типа 'C*' так, чтобы он стал указывать на экземпляр класса A внутри экземпляра класса C. Как компиялтор будет это делать — детали реализации. Никаких требований о том, чтобы null-указатель одного типа при этом первращался именно в null-указатель другого типа, нет и никогда не было. Эти требования не нужны по той простой причине, что стандарт С++ целиком и полностью запрещает такие вызовы. Ни один уважающий себя компилятор не будет тратить время на проверку равенства указателя NULL. Компилятор просто сдвинет указатель на определенное количесво байт. При этом null-указатель перестанет быть null-указателем. По этой самой причине в результате вызова (1) внутри метода 'A::foo' указатель this не будет null-указателем и неравенство 'this_ != NULL' выполнится.

Вот если бы мы сами заставили компилятор выполнить полноценную конверсию указателя при вызове метода

((A*) p)->foo(5); // (3)


тогда результаты выполнения вызовов (2) и (3) совпали бы. Но пользоваться в программах именно такими вызовами только ради того, чтобы сохранить "правильность" преобразования null-указателей, никто, разумеется, не будет. К тому же это потенциальный удар по производительности.

Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.
Best regards,
Андрей Тарасевич
Re[3]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 26.04.02 20:32
Оценка: 34 (5)
Здравствуйте DarkGray, Вы писали:

DG>Написано ли где-нибудь в стандарте, что компилятор не должен закладываться на то, что this всегда не равен нулю?


Может написано, может не написано. Это, в принципе, неважно. Стандарт вместо этого совершенно глухо запрещает все возможные пути падания внутрь метода объекта через null-указатель. А именно: запрещается разадресовывать null-указатель. Это автоматически означает, что, пока поведение программы определено, внутри метода объекта this никак не может быть равен null-указателю.

DG>А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?


Будет приводить к неопределенному поведению. А будет падать или нет — это уже дело десятое. Твоя попытка защититься от такой ситуации путем сравнения 'this' с 'NULL' в рамках языка C++ ситуацию никак не спасает, т.к. у этому моменту ее уже поздно спасать.

Даже если некоторый компилятор все таки делает "корректный" вызов метода через null-указатель, все равно проверка 'this' на NULL в общем случае работать не будет. Т.к., например, нигде не гарантируется, что указатель 'this' базового класса совпадает с указателем 'this' класса-наследника. Даже при одиночном наследовании. А при множественном наследовании твоя проверка в принципе не способна отловить NULL. Посмотри что получится если сделать так:

struct SX { int x; };

struct SB : SX, SA {};

((SB*) 0)->Func();
Best regards,
Андрей Тарасевич
((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 29.04.02 18:05
Оценка: 9 (2)
#Имя: FAQ.cpp.*nullptr
DG>Должен ли по стандарту работать следующий код?

struct SA
{
  void Func() //невиртуальная
  {
    if (this == NULL) 
      return;
  }  
  static void StaticFunc() {}
};

void main()
{
  ((SA*)0)->StaticFunc(); //ИМХО, тут должно быть все нормально
  ((SA*)0)->Func(); //а вот здесь не понятно, мало ли чего может компилятор напихать
}


DG>Написано ли где-нибудь в стандарте, что компилятор не должен закладываться на то, что this всегда не равен нулю?


АТ>>Может написано, может не написано. Это, в принципе, неважно. Стандарт вместо этого совершенно глухо запрещает все возможные пути попадания внутрь метода объекта через null-указатель. А именно: запрещается разадресовывать null-указатель. Это автоматически означает, что, пока поведение программы определено, внутри метода объекта this никак не может быть равен null-указателю.


ПК>AFAIK, вызов метода сам по себе не подразумевает разыменования указателя. Поэтому в стандарте (9.3.1/1) явно указано, что "If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined". Т.к. null-pointer гарантированно не является указателем ни на какой объект, это предложение включает и вызов метода через null-pointer.


Я не согласен. Возможность вызова метода класса (причем как статического, так и нестатического) через null-указатель перекрыта стандартом еще раньше. Стандарт языка подчеркивает, что выражение, стоящее слева от '.' или '->' при доступе к члену класса (методу или полю), обязательно вычисляется. Причем оно вычисляется независимо от того, является ли доступаемый член класса статическим или нет. Вычисление этого выражения автоматически означает разыменование использованного в нем указателя. Разыменование null-указателя порождает неопределенное поведение.

Из приведенной тобой выдержки, кстати, может показаться, что к статическим членам класса можно доступаться через null-указатель. Это не так. Любое вычисляемое выражение, содержащее разыменование null-указателя порождает неопределенное поведение. А вот например параметр 'typeid' может содержать разыменованный null-указатель, т.к. выражение-параметр 'typeid' не вычисляется.

Также можно заметить, что если бы вызов метода не подразумевал разыменования указателя, то стандарт языка С++ должен был бы содержать отдельное замечание по поводу вызова виртуальных методов. Эти последние вызывать через null-указатель все таки нельзя (мы все в глубине души знаем, что такой вызов существенно подразумевает разыменование указателя). Я не нашел в стандарте такого замечания.
Best regards,
Андрей Тарасевич
Re[8]: ((SA*)0)->Func();
От: Alexander Shargin Россия RSDN.ru
Дата: 27.04.02 19:50
Оценка: 15 (1)
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте Alex Fedotov, Вы писали:


AF>>Здравствуйте Андрей Тарасевич, Вы писали:


АТ>>>Для того, чтобы "легализовать" такую реализацию 'GetSafeHwnd' MFC должно либо явно запрещать использование класса 'CWnd' в качестве базового класса в иерархии с множественным наследованием, либо запрещать вызов 'GetSafeHwnd' для экземпляров класса, имеющих множественных предков, либо еще как-то ограничить использование 'GetSafeHwnd'. Вполне может быть, что такие ограничения существуют. Я, правда, о них пока не слышал.


AF>>Таки есть ограничения:

AF>>TN016: Using C++ Multiple Inheritance with MFC

АТ>Это, в некотором смысле, оправдывает вышеприведенный вариант 'GetSafeHwnd', как деталь реализации платформеннозависимой библиотеки.


MFC платформеннонезависимой никогда не планировалась. Она использует не только MS-specific код, но и ассемблерные вставки. Что касается MI, то никто не мешает мне "подмешать" свои классы к классам MFC — с точки зрения MFC это абсолютно законно. Но даже если брать два класса, порождённых от CObject, MI возможно, как показывает пример из той же приведённой Алексом статьи:

#include <afxwin.h>

class CHelloAppAndFrame : public CFrameWnd, public CWinApp
{ 
public:
    CHelloAppAndFrame()
        { }

    // Necessary evil for MI disambiguity
    void* operator new(size_t nSize)
        { return CFrameWnd::operator new(nSize); }
    void operator delete(void* p)
        { CFrameWnd::operator delete(p); }

// Implementation
    // CWinApp overrides
    virtual BOOL InitInstance();
    // CFrameWnd overrides
    virtual void PostNcDestroy();
    afx_msg void OnPaint();

    DECLARE_MESSAGE_MAP()

};

BEGIN_MESSAGE_MAP(CHelloAppAndFrame, CFrameWnd)
    ON_WM_PAINT()
END_MESSAGE_MAP()

// since the frame window is not allocated on the heap, we must
// override PostNCDestroy not to delete the frame object
void CHelloAppAndFrame::PostNcDestroy()
{
    // do nothing (do not call base class)
}

void CHelloAppAndFrame::OnPaint()
{
    CPaintDC dc(this);
    CRect rect;
    GetClientRect(rect);

    CString s = "Hello, Windows!";
    dc.SetTextAlign(TA_BASELINE | TA_CENTER);
    dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
    dc.SetBkMode(TRANSPARENT);
    dc.TextOut(rect.right / 2, rect.bottom / 2, s);
}

// Application initialization
BOOL CHelloAppAndFrame::InitInstance()
{
    // first create the main frame
    if (!CFrameWnd::Create(NULL, "Multiple Inheritance Sample",
        WS_OVERLAPPEDWINDOW, rectDefault))
        return FALSE;

    // the application object is also a frame window
    m_pMainWnd = this;          
    ShowWindow(m_nCmdShow);
    return TRUE;
}

CHelloAppAndFrame theHelloAppAndFrame;
--
Я думал, ты огромный страшный Бажище,
А ты недоучка, крохотный Бажик...
Re[9]: ((SA*)0)->Func();
От: Павел Кузнецов  
Дата: 29.04.02 15:27
Оценка: 7 (1)
Здравствуйте IT, Вы писали:

VVV>>т.е. во втором случае идёт сравнение с 0 и если не 0, то только тогда компилятор приводит тип.


IT>Зря он это делает, может он ещё и проверку выхода за границы массивов мне делать будет


Не зря. Если компилятор не сгенерирует код для проверки на null-pointer при преобразовании указателя, то в последующем, при сравнении полученного после преобразования 0 (NULL) указателя с 0 (NULL), результат может быть самым неожиданным.

class A { int i; };
class B { int i; };
class C : public A, public B { };

C* c = 0;
B* b = c; // если здесь не будет проверки на 0,
          // на типичной 32-битной реализации значением указателя b 
          // станет что-нибудь типа 0x00000004
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Re: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 26.04.02 20:19
Оценка: 6 (1)
Здравствуйте DarkGray, Вы писали:

DG>Должен ли по стандарту работать следующий код?

DG>
DG>struct SA
DG>{
DG>  void Func() //невиртуальная
DG>  {
DG>    if (this == NULL) 
DG>      return;
DG>  }  
DG>  static void StaticFunc() {}
DG>};

DG>void main()
DG>{
DG>  ((SA*)0)->StaticFunc(); //ИМХО, тут должно быть все нормально
DG>  ((SA*)0)->Func(); //а вот здесь не понятно, мало ли чего может компилятор напихать
DG>}
DG>


Нет, не должен. По стандарту, применение оператора '->' к null-указателю, вызывает undefined behavior.
Best regards,
Андрей Тарасевич
Re[3]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 28.04.02 10:47
Оценка: 6 (1)
Здравствуйте DarkGray, Вы писали:

DG>Здравствуйте IT, Вы писали:


IT>>Здравствуйте DarkGray, Вы писали:


DG>>>Должен ли по стандарту работать следующий код?


IT>>Давай перепишем и посмотрим причём тут стандарт:


IT>>
IT>>struct SA
IT>>{
IT>>  void Func(); //невиртуальная и не важно

DG>//как раз важно что функция не виртуальная, для виртуальной функции все будет падать со свистом при нулевом указателе

IT>>  static void StaticFunc();
IT>>};

IT>>void f(SA*)
IT>>{
IT>>  SA->Func(); // откуда компилятору знать, что у программера на уме
IT>>}

IT>>void main()
IT>>{
IT>>  SA::StaticFunc(); // можно обойтись и без объекта
IT>>  f((SA*)0);        // мало ли чего можно компилятору подсунуть
IT>>}
IT>>


DG>Наверное, не правильно вопрос задал...


DG>Написано ли где-нибудь в стандарте, что компилятор не должен закладываться на то, что this всегда не равен нулю?


DG>Для виртуальный функций — понятно. Компилятор не явно закладывается на то, что this не нуль и этот указатель использует в своих целях, по нему ищет таблицу виртуальных функции.

DG>В случае нулевого указателя все падает.

DG>Для статик функции то же — понятно, так как this-а у них совсем нет.


DG>А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?


Если посмотреть с другой стороны, то любой вызов C++ функции можно описать в терминам C функции, т.е. это будет функция, где первым параметром передаётся указатель на какую-то структуру даннык (this). Посему, при вызове такой функции из C этот параметр (this) можно задать каким угодно (и NULL в том числе). Например, если посмотреть МАПИшные примеры, то там все указатели проверяются на валидность, например так:

/* BAD_STANDARD_OBJ
 *
 * This macro insures that the object is a writable object of the correct size
 * and that this method belongs to the object.
 *
 * NOTES ON USE!
 *  This depends upon using the standard method of declaring the object
 *  interface.
 *
 *  prefix is the method prefix you chose when declaring the object interface.
 *  method is the standard method name of the calling method.
 *  lpVtbl is the name of the lpVtbl element of your object.
 */
#define BAD_STANDARD_OBJ( lpObj, prefix, method, lpVtbl)     (   IsBadWritePtr( (lpObj), sizeof(*lpObj))      || IsBadReadPtr( (void *) &(lpObj->lpVtbl->method), sizeof(LPVOID))      ||( ( LPVOID) (lpObj->lpVtbl->method) != (LPVOID) (prefix##method)))

#define FBadUnknown( lpObj )     (   IsBadReadPtr( (lpObj), sizeof(LPVOID) )      || IsBadReadPtr( (lpObj)->lpVtbl, 3 * sizeof(LPUNKNOWN) )      || IsBadCodePtr( (FARPROC)(lpObj)->lpVtbl->QueryInterface ))
Re[5]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 27.04.02 17:51
Оценка: 5 (1)
Здравствуйте Alexander Shargin, Вы писали:

AF>>Как Андрей уже сказал, вызов метода с недействительным значением this приводит к undefined behavior. Это, однако, не останавливает разработчиков некоторых библиотек он написания вот такого кода:


AF>>
AF>>_AFXWIN_INLINE HWND CWnd::GetSafeHwnd() const
AF>>    { return this == NULL ? NULL : m_hWnd; }
AF>>


AS>От себя добавлю, что при реализации стандартной библиотеки также иногда прибегают к разименованию нулевого указателя. Например, так реализован макрос offsetof, который интенсивно используется как в MFC, так и в ATL:


AS>
AS>#define offsetof(s,m)   (size_t)&(((s *)0)->m)
AS>


Реализация стандартной библиотеки имеет право опираться на любые implementation defined особенности платформы, в том числе на определенность "неопределенного поведения". При этом, разумеется, требуется, чтобы поведение оставалось определенным для всех легальных применений данной конструкции. Для вышеприведенной реализации 'offsetof' это условие выполняется. Для 'GetSafeHwnd' — нет.

Для того, чтобы "легализовать" такую реализацию 'GetSafeHwnd' MFC должно либо явно запрещать использование класса 'CWnd' в качестве базового класса в иерархии с множественным наследованием, либо запрещать вызов 'GetSafeHwnd' для экземпляров класса, имеющих множественных предков, либо еще как-то ограничить использование 'GetSafeHwnd'. Вполне может быть, что такие ограничения существуют. Я, правда, о них пока не слышал.
Best regards,
Андрей Тарасевич
Re[3]: ((SA*)0)->Func();
От: Alex Fedotov США  
Дата: 26.04.02 20:36
Оценка: 3 (1)
Здравствуйте DarkGray, Вы писали:

DG>А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?


Как Андрей уже сказал, вызов метода с недействительным значением this приводит к undefined behavior. Это, однако, не останавливает разработчиков некоторых библиотек он написания вот такого кода:

_AFXWIN_INLINE HWND CWnd::GetSafeHwnd() const
    { return this == NULL ? NULL : m_hWnd; }
-- Alex Fedotov
Re[5]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 27.04.02 06:00
Оценка: 3 (1)
Здравствуйте Silver_s, Вы писали:

SS>Например объявить функцию за пределами класса:


SS>
SS>inline HWND GetSafeHwnd(CWnd *pW)
SS>{
SS>   return pW == NULL ? NULL : pW->m_hWnd; 
SS>}
SS>

SS>Здесь вроде придратся не к чему (даже плохим стилем не обзовешь), но это абсолютно то же самое что и в первом случае.

Нет, это не абсолютно то же самое. Теперь, чтобы вызвать эту функцию для указателя на класс, унаследованный от 'CWnd', компилятору придется выполнить конверсию типа указателя. При выполнении конверсии гарантируется, что null-указатель одного типа будет сконвертирован именно в null-указатель другого типа. Независимо от типа наследования — одиночное или множественное. Поэтому такой вариант с внешней функцией гарантированно будет работать всегда. А работоспособность исходного варианта с функцией-членом ничем не гарантирована. В случае множественного наследования исходный вариант просто сядет в лужу.

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


Если проверка на NULL делается во внешней функции, то никаких проблем с множественным наследованием нет. Даже задумываться об этом не надо.
Best regards,
Андрей Тарасевич
Re[4]: ((SA*)0)->Func();
От: Павел Кузнецов  
Дата: 29.04.02 11:47
Оценка: 3 (1)
Здравствуйте Андрей Тарасевич, Вы писали:

DG>>Написано ли где-нибудь в стандарте, что компилятор не должен закладываться на то, что this всегда не равен нулю?


АТ>Может написано, может не написано. Это, в принципе, неважно. Стандарт вместо этого совершенно глухо запрещает все возможные пути падания внутрь метода объекта через null-указатель. А именно: запрещается разадресовывать null-указатель. Это автоматически означает, что, пока поведение программы определено, внутри метода объекта this никак не может быть равен null-указателю.


AFAIK, вызов метода сам по себе не подразумевает разыменования указателя. Поэтому в стандарте (9.3.1/1) явно указано, что "If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined". Т.к. null-pointer гарантированно не является указателем ни на какой объект, это предложение включает и вызов метода через null-pointer.
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Re[6]: ((SA*)0)->Func();
От: Alex Fedotov США  
Дата: 27.04.02 19:29
Оценка: +1
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Для того, чтобы "легализовать" такую реализацию 'GetSafeHwnd' MFC должно либо явно запрещать использование класса 'CWnd' в качестве базового класса в иерархии с множественным наследованием, либо запрещать вызов 'GetSafeHwnd' для экземпляров класса, имеющих множественных предков, либо еще как-то ограничить использование 'GetSafeHwnd'. Вполне может быть, что такие ограничения существуют. Я, правда, о них пока не слышал.


Таки есть ограничения:
TN016: Using C++ Multiple Inheritance with MFC
-- Alex Fedotov
Re[9]: ((SA*)0)->Func();
От: Sergey Россия  
Дата: 29.04.02 06:05
Оценка: +1
Здравствуйте Alexander Shargin, Вы писали:


AS>MFC платформеннонезависимой никогда не планировалась.


Неправда ваша Если мне окончательно не отшибло память, то та версия MFC, что шла с VC 4.0, существовала не только для Win32, но и для Mac OS.

AS>Она использует не только MS-specific код, но и ассемблерные вставки.

Ну дык реализация библиотеки для конкретной платформы могет делать что ей заблагорассудится. Сишная RTL от MS тоже кое-где на ассемблере писана, главное чтоб интерфейс был одинаковымю
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
((SA*)0)->Func();
От: DarkGray Россия http://blog.metatech.ru/post/ogni-razrabotki.aspx
Дата: 26.04.02 19:52
Оценка:
Должен ли по стандарту работать следующий код?
struct SA
{
  void Func() //невиртуальная
  {
    if (this == NULL) 
      return;
  }  
  static void StaticFunc() {}
};

void main()
{
  ((SA*)0)->StaticFunc(); //ИМХО, тут должно быть все нормально
  ((SA*)0)->Func(); //а вот здесь не понятно, мало ли чего может компилятор напихать
}
Re: ((SA*)0)->Func();
От: IT Россия linq2db.com
Дата: 26.04.02 20:02
Оценка:
Здравствуйте DarkGray, Вы писали:

DG>Должен ли по стандарту работать следующий код?


Давай перепишем и посмотрим причём тут стандарт:

struct SA
{
  void Func(); //невиртуальная и не важно
  static void StaticFunc();
};

void f(SA*)
{
  SA->Func(); // откуда компилятору знать, что у программера на уме
}

void main()
{
  SA::StaticFunc(); // можно обойтись и без объекта
  f((SA*)0);        // мало ли чего можно компилятору подсунуть
}
Если нам не помогут, то мы тоже никого не пощадим.
Re[2]: ((SA*)0)->Func();
От: DarkGray Россия http://blog.metatech.ru/post/ogni-razrabotki.aspx
Дата: 26.04.02 20:17
Оценка:
Здравствуйте IT, Вы писали:

IT>Здравствуйте DarkGray, Вы писали:


DG>>Должен ли по стандарту работать следующий код?


IT>Давай перепишем и посмотрим причём тут стандарт:


IT>
IT>struct SA
IT>{
IT>  void Func(); //невиртуальная и не важно

//как раз важно что функция не виртуальная, для виртуальной функции все будет падать со свистом при нулевом указателе

IT>  static void StaticFunc();
IT>};

IT>void f(SA*)
IT>{
IT>  SA->Func(); // откуда компилятору знать, что у программера на уме
IT>}

IT>void main()
IT>{
IT>  SA::StaticFunc(); // можно обойтись и без объекта
IT>  f((SA*)0);        // мало ли чего можно компилятору подсунуть
IT>}
IT>


Наверное, не правильно вопрос задал...

Написано ли где-нибудь в стандарте, что компилятор не должен закладываться на то, что this всегда не равен нулю?

Для виртуальный функций — понятно. Компилятор не явно закладывается на то, что this не нуль и этот указатель использует в своих целях, по нему ищет таблицу виртуальных функции.
В случае нулевого указателя все падает.

Для статик функции то же — понятно, так как this-а у них совсем нет.

А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?
Re[3]: ((SA*)0)->Func();
От: IT Россия linq2db.com
Дата: 26.04.02 20:26
Оценка:
Здравствуйте DarkGray, Вы писали:

DG>А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?


Может быть, если твой метод имеет доступ к членам класса или вызывает другие методы, которые это делают. И точно будет, если твой метод вызывает виртуальные функции.
Если нам не помогут, то мы тоже никого не пощадим.
Re[4]: ((SA*)0)->Func();
От: Silver_s Ниоткуда  
Дата: 27.04.02 05:52
Оценка:
Здравствуйте Alex Fedotov, Вы писали:

AF>Как Андрей уже сказал, вызов метода с недействительным значением this приводит к undefined behavior. Это, однако, не останавливает разработчиков некоторых библиотек он написания вот такого кода:


AF>
AF>_AFXWIN_INLINE HWND CWnd::GetSafeHwnd() const
AF>    { return this == NULL ? NULL : m_hWnd; }
AF>


А как бы им надо было написать чтобы реализовать такую функциональность и соответствовать стандарту(ну или не использовать плохой стиль программирования)
Например объявить функцию за пределами класса:

inline HWND GetSafeHwnd(CWnd *pW)
{
   return pW == NULL ? NULL : pW->m_hWnd; 
}

Здесь вроде придратся не к чему (даже плохим стилем не обзовешь), но это абсолютно то же самое что и в первом случае.
А в случае множественного наследования проблема с null указателями есть не только у функций членов. Просто не верно утверждение "если производный класс null то и базовые классы null" и это прийдется учитывать всегда.
И что плохого в том, что в не virtual функции используется this==null, в конце концов после компиляции в obj файле она будет лежать как обычная функция, у которой первый параметр сравнивается с 0.
Re[5]: ((SA*)0)->Func();
От: Кодт Россия  
Дата: 27.04.02 06:05
Оценка:
Здравствуйте Silver_s, Вы писали:

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

SS>И что плохого в том, что в не virtual функции используется this==null, в конце концов после компиляции в obj файле она будет лежать как обычная функция, у которой первый параметр сравнивается с 0.

Короче, по стандарту не написано, поэтому все компиляторы наплюют на нуллевой this (а могли бы, могли бы! Например, считать, что NULL-потомок имеет всех NULL-предков).

Непонятно, правда — как реагировать тогда, когда нужно разыменовать NULL.

(BYTE*)(NULL)[0] = 1; // исключение защиты памяти
(CObject)(NULL)->VirtualFn(); // в принципе, то же, но — не факт, не факт...

Да и код утяжелится проверками.
Перекуём баги на фичи!
Re[4]: ((SA*)0)->Func();
От: Alexander Shargin Россия RSDN.ru
Дата: 27.04.02 17:37
Оценка:
Здравствуйте Alex Fedotov, Вы писали:

AF>Как Андрей уже сказал, вызов метода с недействительным значением this приводит к undefined behavior. Это, однако, не останавливает разработчиков некоторых библиотек он написания вот такого кода:


AF>
AF>_AFXWIN_INLINE HWND CWnd::GetSafeHwnd() const
AF>    { return this == NULL ? NULL : m_hWnd; }
AF>


От себя добавлю, что при реализации стандартной библиотеки также иногда прибегают к разименованию нулевого указателя. Например, так реализован макрос offsetof, который интенсивно используется как в MFC, так и в ATL:

#define offsetof(s,m)   (size_t)&(((s *)0)->m)
--
Я думал, ты огромный страшный Бажище,
А ты недоучка, крохотный Бажик...
Re[7]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 27.04.02 19:34
Оценка:
Здравствуйте Alex Fedotov, Вы писали:

AF>Здравствуйте Андрей Тарасевич, Вы писали:


АТ>>Для того, чтобы "легализовать" такую реализацию 'GetSafeHwnd' MFC должно либо явно запрещать использование класса 'CWnd' в качестве базового класса в иерархии с множественным наследованием, либо запрещать вызов 'GetSafeHwnd' для экземпляров класса, имеющих множественных предков, либо еще как-то ограничить использование 'GetSafeHwnd'. Вполне может быть, что такие ограничения существуют. Я, правда, о них пока не слышал.


AF>Таки есть ограничения:

AF>TN016: Using C++ Multiple Inheritance with MFC

Это, в некотором смысле, оправдывает вышеприведенный вариант 'GetSafeHwnd', как деталь реализации платформеннозависимой библиотеки.
Best regards,
Андрей Тарасевич
Re[9]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 28.04.02 04:56
Оценка:
Здравствуйте Alexander Shargin, Вы писали:

AF>>>Таки есть ограничения:

AF>>>TN016: Using C++ Multiple Inheritance with MFC

АТ>>Это, в некотором смысле, оправдывает вышеприведенный вариант 'GetSafeHwnd', как деталь реализации платформеннозависимой библиотеки.


AS>MFC платформеннонезависимой никогда не планировалась. Она использует не только MS-specific код, но и ассемблерные вставки.


Я так и написал: "...как деталь реализации платформеннозависимой библиотеки..."

AS>Что касается MI, то никто не мешает мне "подмешать" свои классы к классам MFC — с точки зрения MFC это абсолютно законно.


Да, законно. При выполненияи того условия, что 'CWnd' будет первой базой любого унаследованного класса, как сказано в статье.
Best regards,
Андрей Тарасевич
Re[5]: ((SA*)0)->Func();
От: Silver_s Ниоткуда  
Дата: 29.04.02 05:58
Оценка:
Да, оказывается такое преобразование есть.
class C:A,B{...
При преобразовании от B к C в disassembly виднеются сравнения с 0 и условные переходы.
Re[5]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 08:02
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте VVV, Вы писали:



VVV>>Если посмотреть с другой стороны, то любой вызов C++ функции можно описать в терминам C функции, т.е. это будет функция, где первым параметром передаётся указатель на какую-то структуру даннык (this). Посему, при вызове такой функции из C этот параметр (this) можно задать каким угодно (и NULL в том числе).


АТ>В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.


АТ>Предположим, что у нас имеется вот такой класс A


АТ>
АТ>struct A 
АТ>{ 
АТ>  void foo(int i) { cout << (this != NULL ? "not NULL" : "NULL") << " " << i << endl; }
АТ>};
АТ>


АТ>Перепишем метод 'A::foo' в виде "эквивалентной" С функции, как ты предлагаешь (если я тебя правильно понял):


АТ>
АТ>void foo(A* this_, int i) { cout << (this_ != NULL ? "not NULL" : "NULL") << " " << i << endl; }
АТ>


АТ>Пусть от класса A и класса B множественно унаследован класс C:


АТ>
АТ>struct B { int x; };

АТ>struct C : B, A {};
АТ>


АТ>Рассмотрим вот такой вызов метода 'foo' через null-указатель типа 'C*':


АТ>
АТ>C* p = NULL;

p->>foo(5); // (1)
АТ>


АТ>Как будет выглядеть соответствующий вызов С функции 'foo'? Он будет выглядеть вот так:


АТ>
АТ>foo(p, 5); // (2)
АТ>


АТ>Эквивалентны ли эти вызовы (закроем глаза на неопределенное поведение в вызове (1))? Нет, не эквивалентны. При компиляции, например, MSVC++, эти вызовы распечатают разные результаты на экране. Почему так происходит?


АТ>Происходит это потому, что в вызове (2) компилятор вынужден сделать стандартную pointer conversion — конверсию типа первого параметра от типа 'C*' к типу 'A*'. При этом, согласно спецификации языка С++, null-указатель одного типа должен быть переведен в null-указатель другого типа. По этой причине в результате вызова (2) внутрь функции 'foo' будет передан null-указатель и неравенство 'this_ != NULL' не выполнится.


АТ>А в вызове (1) ситуация совершенно иная. Для того, чтобы взывать метод класса A через указатель типа 'C*', этот указатель тоже необходимо определенным образом "преобразовать". То, что происходит с указателем в таком случае, не является никакой "официальной" конверсией языка С++. Компилятор обязан просто как-то "сдвинуть" указатель типа 'C*' так, чтобы он стал указывать на экземпляр класса A внутри экземпляра класса C. Как компиялтор будет это делать — детали реализации. Никаких требований о том, чтобы null-указатель одного типа при этом первращался именно в null-указатель другого типа, нет и никогда не было. Эти требования не нужны по той простой причине, что стандарт С++ целиком и полностью запрещает такие вызовы. Ни один уважающий себя компилятор не будет тратить время на проверку равенства указателя NULL. Компилятор просто сдвинет указатель на определенное количесво байт. При этом null-указатель перестанет быть null-указателем. По этой самой причине в результате вызова (1) внутри метода 'A::foo' указатель this не будет null-указателем и неравенство 'this_ != NULL' выполнится.


АТ>Вот если бы мы сами заставили компилятор выполнить полноценную конверсию указателя при вызове метода


АТ>
АТ>((A*) p)->foo(5); // (3)
АТ>


АТ>тогда результаты выполнения вызовов (2) и (3) совпали бы. Но пользоваться в программах именно такими вызовами только ради того, чтобы сохранить "правильность" преобразования null-указателей, никто, разумеется, не будет. К тому же это потенциальный удар по производительности.


АТ>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.



Я и не говорил, что это правильно или так надо делать или при этом не будет проблем , но вопрос был такой:


...
А вот для обычных функций не понятно. Могу ли я в качестве this при вызове использовать произвольное значение (в частности NULL) или не могу? То есть будет это приводить к падению или не будет?


и я показал, как в качестве this передать произвольное значение и не более того.

опять же привёл пример как проверять указатели на валидность через IsBadReadPtr...
Re[5]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 08:22
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте VVV, Вы писали:



VVV>>Если посмотреть с другой стороны, то любой вызов C++ функции можно описать в терминам C функции, т.е. это будет функция, где первым параметром передаётся указатель на какую-то структуру даннык (this). Посему, при вызове такой функции из C этот параметр (this) можно задать каким угодно (и NULL в том числе).


АТ>В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.


АТ>Предположим, что у нас имеется вот такой класс A


АТ>
АТ>struct A 
АТ>{ 
АТ>  void foo(int i) { cout << (this != NULL ? "not NULL" : "NULL") << " " << i << endl; }
АТ>};
АТ>


АТ>Перепишем метод 'A::foo' в виде "эквивалентной" С функции, как ты предлагаешь (если я тебя правильно понял):


АТ>
АТ>void foo(A* this_, int i) { cout << (this_ != NULL ? "not NULL" : "NULL") << " " << i << endl; }
АТ>


АТ>Пусть от класса A и класса B множественно унаследован класс C:


АТ>
АТ>struct B { int x; };

АТ>struct C : B, A {};
АТ>


АТ>Рассмотрим вот такой вызов метода 'foo' через null-указатель типа 'C*':


АТ>
АТ>C* p = NULL;

p->>foo(5); // (1)
АТ>


АТ>Как будет выглядеть соответствующий вызов С функции 'foo'? Он будет выглядеть вот так:


АТ>
АТ>foo(p, 5); // (2)
АТ>


АТ>Эквивалентны ли эти вызовы (закроем глаза на неопределенное поведение в вызове (1))? Нет, не эквивалентны. При компиляции, например, MSVC++, эти вызовы распечатают разные результаты на экране. Почему так происходит?


АТ>Происходит это потому, что в вызове (2) компилятор вынужден сделать стандартную pointer conversion — конверсию типа первого параметра от типа 'C*' к типу 'A*'. При этом, согласно спецификации языка С++, null-указатель одного типа должен быть переведен в null-указатель другого типа. По этой причине в результате вызова (2) внутрь функции 'foo' будет передан null-указатель и неравенство 'this_ != NULL' не выполнится.


АТ>А в вызове (1) ситуация совершенно иная. Для того, чтобы взывать метод класса A через указатель типа 'C*', этот указатель тоже необходимо определенным образом "преобразовать". То, что происходит с указателем в таком случае, не является никакой "официальной" конверсией языка С++. Компилятор обязан просто как-то "сдвинуть" указатель типа 'C*' так, чтобы он стал указывать на экземпляр класса A внутри экземпляра класса C. Как компиялтор будет это делать — детали реализации. Никаких требований о том, чтобы null-указатель одного типа при этом первращался именно в null-указатель другого типа, нет и никогда не было. Эти требования не нужны по той простой причине, что стандарт С++ целиком и полностью запрещает такие вызовы. Ни один уважающий себя компилятор не будет тратить время на проверку равенства указателя NULL. Компилятор просто сдвинет указатель на определенное количесво байт. При этом null-указатель перестанет быть null-указателем. По этой самой причине в результате вызова (1) внутри метода 'A::foo' указатель this не будет null-указателем и неравенство 'this_ != NULL' выполнится.


АТ>Вот если бы мы сами заставили компилятор выполнить полноценную конверсию указателя при вызове метода


АТ>
АТ>((A*) p)->foo(5); // (3)
АТ>


АТ>тогда результаты выполнения вызовов (2) и (3) совпали бы. Но пользоваться в программах именно такими вызовами только ради того, чтобы сохранить "правильность" преобразования null-указателей, никто, разумеется, не будет. К тому же это потенциальный удар по производительности.


АТ>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.


не ради разведения полемики, а ради истины


#include "stdafx.h"
#include <iostream.h>

struct A{
  void foo(int i) 
  { 
      cout << hex << this  << " " << i << endl; 
  }
};

void foo(A* this_, int i) 
{ 
    cout << hex << this_  << " " << i << endl; 
}


struct B{
    int x;
};

struct C: B, A{
};

int main(int argc, char* argv[])
{
    C *p=(C*)1;
    p->foo(5);

    foo(p, 5);

    return 0;
}


оказалось, что как раз VC 6.0 в случае foo(p, 5); проверяет p на 0 и не делает смещения в 4 байта, только если p равен NULL, в приведённом же примере где C *p=(C*)1 this и this_ совпадают.
Re[5]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 12:49
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте VVV, Вы писали:



...
АТ>В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.

...
АТ>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.

Неверно. Все COM интерфейсы описаны и как C++ классы и как структуры на C и вызовы из C с передачей THIS первым параметром очень даже хорошо работают, хотя мы знаем, что реализуются COM объекты в большинстве случаев как классы с множественным наследованием.

    // *** IUnknown methods ***
    STDMETHOD(QueryInterface) (THIS_ REFIID riid, LPVOID FAR * lppvObj) PURE;
    STDMETHOD_(ULONG,AddRef) (THIS)  PURE;
    STDMETHOD_(ULONG,Release) (THIS) PURE;


Проблема, приведённая Вами проявляется только на указателе == NULL, потому что компилятор пытается оставить NULL-указатель инвариантом и не делает ему приведение типов, во всех остальных случаях С вызовы с первым this работают так же как и C++ (иначе как бы тогда работал COM???).
Re[6]: ((SA*)0)->Func();
От: IT Россия linq2db.com
Дата: 29.04.02 13:17
Оценка:
Здравствуйте VVV, Вы писали:

АТ>>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.


VVV>Неверно. Все COM интерфейсы описаны и как C++ классы и как структуры на C и вызовы из C с передачей THIS первым параметром очень даже хорошо работают, хотя мы знаем, что реализуются COM объекты в большинстве случаев как классы с множественным наследованием.


Ты не совсем внимательно читал поедыдущий постинг. Множественное наследование интерфейсов (я позволю себе ввести такой термин) и наследование регулярных классов C++ немного отличаются. Интерфейсы можно рассматривать как упрощённые классы, но упрощенные настолько, что это упрощение снимает большинство проблем связанных с множественным наследованием. У интерфейсов нет данных, а это самая большая проблема при множественном наследовании и у Андрея в "экземпляр класса A внутри экземпляра класса C" как раз и идёт речь не об указателях на таблицы виртуальных методов, а именно об экземплярах данных этих классов.

Что касается интерфейсов, то они так или иначе поддерживаются уже большинством языков, в том числе вошли как стандарт в .NET и никаких проблем с множественным наследованием у них на возникает. Это просто группа указателей в общей таблице виртуальных методов, не более того.

VVV>Проблема, приведённая Вами проявляется только на указателе == NULL, потому что компилятор пытается оставить NULL-указатель инвариантом и не делает ему приведение типов, во всех остальных случаях С вызовы с первым this работают так же как и C++ (иначе как бы тогда работал COM???).


Компилятору по барабану какое там значение у указателя, NULL или не NULL.
Если нам не помогут, то мы тоже никого не пощадим.
Re[7]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 13:47
Оценка:
Здравствуйте IT, Вы писали:

IT>Здравствуйте VVV, Вы писали:


АТ>>>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.


VVV>>Неверно. Все COM интерфейсы описаны и как C++ классы и как структуры на C и вызовы из C с передачей THIS первым параметром очень даже хорошо работают, хотя мы знаем, что реализуются COM объекты в большинстве случаев как классы с множественным наследованием.


IT>Ты не совсем внимательно читал поедыдущий постинг. Множественное наследование интерфейсов (я позволю себе ввести такой термин) и наследование регулярных классов C++ немного отличаются. Интерфейсы можно рассматривать как упрощённые классы, но упрощенные настолько, что это упрощение снимает большинство проблем связанных с множественным наследованием. У интерфейсов нет данных, а это самая большая проблема при множественном наследовании и у Андрея в "экземпляр класса A внутри экземпляра класса C" как раз и идёт речь не об указателях на таблицы виртуальных методов, а именно об экземплярах данных этих классов.


IT>Что касается интерфейсов, то они так или иначе поддерживаются уже большинством языков, в том числе вошли как стандарт в .NET и никаких проблем с множественным наследованием у них на возникает. Это просто группа указателей в общей таблице виртуальных методов, не более того.


VVV>>Проблема, приведённая Вами проявляется только на указателе == NULL, потому что компилятор пытается оставить NULL-указатель инвариантом и не делает ему приведение типов, во всех остальных случаях С вызовы с первым this работают так же как и C++ (иначе как бы тогда работал COM???).


IT>Компилятору по барабану какое там значение у указателя, NULL или не NULL.


В том то и дело, что не "по барабану"...

из предыдущего примера:
int main(int argc, char* argv[])
{
    C *p=(C*)NULL;
    p->foo(5);

    foo(p, 5);

    return 0;
}

теперь ассемблер
27:   int main(int argc, char* argv[])
28:   {
00411680   push        ebp
00411681   mov         ebp,esp
00411683   sub         esp,48h
00411686   push        ebx
00411687   push        esi
00411688   push        edi
00411689   lea         edi,[ebp-48h]
0041168C   mov         ecx,12h
00411691   mov         eax,0CCCCCCCCh
00411696   rep stos    dword ptr [edi]
29:       C *p=(C*)NULL;
00411698   mov         dword ptr [ebp-4],0
30:       p->foo(5);
0041169F   push        5
004116A1   mov         ecx,dword ptr [ebp-4]
004116A4   add         ecx,4
004116A7   call        @ILT+15(A::foo) (00401014)
31:
32:       foo(p, 5);
004116AC   cmp         dword ptr [ebp-4],0
004116B0   je          main+3Dh (004116bd)
004116B2   mov         eax,dword ptr [ebp-4]
004116B5   add         eax,4
004116B8   mov         dword ptr [ebp-8],eax
004116BB   jmp         main+44h (004116c4)
004116BD   mov         dword ptr [ebp-8],0
004116C4   push        5
004116C6   mov         ecx,dword ptr [ebp-8]
004116C9   push        ecx
004116CA   call        @ILT+55(foo) (0040103c)
004116CF   add         esp,8
33:
34:       return 0;
004116D2   xor         eax,eax
35:   }


т.е. во втором случае идёт сравнение с 0 и если не 0, то только тогда компилятор приводит тип.


насчёт наследования:
class Obj1 : public IUnknown{
  virtual int Sum();
  int x;
  int y;
  char buf[128];
};

class Obj2 : public IUnknown{
  virtual int Get();
  int q;
  int w;
  char name[256];
};

class MainObj: public IUnknown, public Obj1, public Obj2{
  int zzz;
  int xxx;
};


разве это не нормальный COM объект? (конечно, наружу я выставлю примерно такое:
class IObj1:public IUnknown{
public:
  virtual int Sum()=0;
}

и т.д.
)
Re[6]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 29.04.02 14:15
Оценка:
Здравствуйте VVV, Вы писали:

VVV>...

АТ>>В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.

VVV>...

АТ>>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.

VVV>Неверно. Все COM интерфейсы описаны и как C++ классы и как структуры на C и вызовы из C с передачей THIS первым параметром очень даже хорошо работают, хотя мы знаем, что реализуются COM объекты в большинстве случаев как классы с множественным наследованием.


Что значит "неверно"? В-первых, реализация COM-интерфейсов зафиксирована на очень низком уровне (бинараная совместимось), и "подтасована" под организацию таблиц виртуальных методов конкретным компилятором. К языку С++ это никакого отношения не имеет. Мое же утверждение относилось именно к языку С++.

VVV>Проблема, приведённая Вами проявляется только на указателе == NULL, потому что компилятор пытается оставить NULL-указатель инвариантом и не делает ему приведение типов, во всех остальных случаях С вызовы с первым this работают так же как и C++ (иначе как бы тогда работал COM???).


Совершенно верно. Эта проблема проявляется только на null-указателе. Я никогда не говорил, что она будет проявляться на каких-то других указателях. Примера с null совершенно достаточно для того, чтобы сделать то утверждение, которое я сделал. Где тут "неверно"?

И я все таки не понимаю, при чем тут COM. В COM в качестве указателя на интерфейс null-указатель использован быть не может.
Best regards,
Андрей Тарасевич
Re[6]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 29.04.02 14:23
Оценка:
Здравствуйте VVV, Вы писали:

VVV>не ради разведения полемики, а ради истины

VVV>...
VVV>int main(int argc, char* argv[])
VVV>{
VVV> C *p=(C*)1;
VVV> p->foo(5);

VVV> foo(p, 5);


VVV> return 0;

VVV>}
VVV>[/ccode]

VVV>оказалось, что как раз VC 6.0 в случае foo(p, 5); проверяет p на 0 и не делает смещения в 4 байта, только если p равен NULL, в приведённом же примере где C *p=(C*)1 this и this_ совпадают.


Разумеется. Об этом и идет речь в моем предыдущем сообщении. Так что именно ты хочешь этим сказать?
Best regards,
Андрей Тарасевич
Re[8]: ((SA*)0)->Func();
От: IT Россия linq2db.com
Дата: 29.04.02 15:03
Оценка:
Здравствуйте VVV, Вы писали:

VVV>В том то и дело, что не "по барабану"...

VVV>т.е. во втором случае идёт сравнение с 0 и если не 0, то только тогда компилятор приводит тип.

Зря он это делает, может он ещё и проверку выхода за границы массивов мне делать будет

VVV>насчёт наследования:

[skip]
VVV> разве это не нормальный COM объект?

Это нормальный C++ класс, реализующий интерфейс IUnknown. COM объектом он станет, когда ты его создашь посредством одной из функций в контексте COM.

VVV>(конечно, наружу я выставлю примерно такое:


Вот-вот, и это правильно, наружу выставляется именно интерфейс.
Хочу ещё раз повториться. Интерфейс — это не полноценный C++ класс. Упрощенно его реализацию можно рассматривать как указатель на последовательную группу виртуальных функций внутри общей таблицы виртуальных методов. При этом никакие данные (поля) класса его не касаются. В языках, где запрещено множественное наследование классов, как правило разрешено множественное наследование интерфейсов, т.к. это легко реализуемо и не вносит в описание класса неразберихи.
Если нам не помогут, то мы тоже никого не пощадим.
Re[10]: ((SA*)0)->Func();
От: IT Россия linq2db.com
Дата: 29.04.02 15:35
Оценка:
Здравствуйте Павел Кузнецов, Вы писали:

ПК>Не зря. Если компилятор не сгенерирует код для проверки на null-pointer при преобразовании указателя, то в последующем, при сравнении полученного после преобразования 0 (NULL) указателя с 0 (NULL), результат может быть самым неожиданным.


Да, ты прав. Я закциклился на первоначальной теме топика
Если нам не помогут, то мы тоже никого не пощадим.
Re[7]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 17:25
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте VVV, Вы писали:


VVV>>оказалось, что как раз VC 6.0 в случае foo(p, 5); проверяет p на 0 и не делает смещения в 4 байта, только если p равен NULL, в приведённом же примере где C *p=(C*)1 this и this_ совпадают.


АТ>Разумеется. Об этом и идет речь в моем предыдущем сообщении. Так что именно ты хочешь этим сказать?


То же, что и в самом начале — что вызовы C++ функций могут быть проэмулированы вызовами C функций с передачей this первым параметром.
Re[8]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 29.04.02 17:33
Оценка:
Здравствуйте VVV, Вы писали:

АТ>>Разумеется. Об этом и идет речь в моем предыдущем сообщении. Так что именно ты хочешь этим сказать?


VVV>То же, что и в самом начале — что вызовы C++ функций могут быть проэмулированы вызовами C функций с передачей this первым параметром.


В рамках легального С++ — да, может быть проэмулирована. Так что я с тобой полностью согласен — в рамках легального С++.

Но в этой дискуссии речь идет о [нелегальном] вызове метода класса через null-указатель не объект (обрати внимание на поле "тема"). Как только речь идет о вызове метода класса через null-указатель на объект, обычный вызов метода и вышеупомянутая эмуляция дают (могут давать) разные результаты. Я хотел сказать только это и ничего больше.
Best regards,
Андрей Тарасевич
Re[7]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 29.04.02 17:49
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте VVV, Вы писали:


VVV>>...

АТ>>>В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++ нельзя так запросто описать в терминах C функций.

VVV>>...

АТ>>>Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.

VVV>>Неверно. Все COM интерфейсы описаны и как C++ классы и как структуры на C и вызовы из C с передачей THIS первым параметром очень даже хорошо работают, хотя мы знаем, что реализуются COM объекты в большинстве случаев как классы с множественным наследованием.


АТ>Что значит "неверно"? В-первых, реализация COM-интерфейсов зафиксирована на очень низком уровне (бинараная совместимось), и "подтасована" под организацию таблиц виртуальных методов конкретным компилятором. К языку С++ это никакого отношения не имеет. Мое же утверждение относилось именно к языку С++.


VVV>>Проблема, приведённая Вами проявляется только на указателе == NULL, потому что компилятор пытается оставить NULL-указатель инвариантом и не делает ему приведение типов, во всех остальных случаях С вызовы с первым this работают так же как и C++ (иначе как бы тогда работал COM???).


АТ>Совершенно верно. Эта проблема проявляется только на null-указателе. Я никогда не говорил, что она будет проявляться на каких-то других указателях. Примера с null совершенно достаточно для того, чтобы сделать то утверждение, которое я сделал. Где тут "неверно"?


АТ>И я все таки не понимаю, при чем тут COM. В COM в качестве указателя на интерфейс null-указатель использован быть не может.


Неверно следующее высказывание

Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.


Просто обратил Ваше внимание на то, что множественное наследование в данном случае не имеет никакого значения (исключение составляет только this == NULL и то будем считать, что в функции он проверяется не просто this == NULL а с помощью IsBadReadPtr), ведь COM объекты реализуются с помощью множественного наследование (в большинстве случаев) и прекрасно вызываются из C — это же самое утверждение справедливо не только для COM, но и просто для любых объектов C++. COM был назван, как самое очевидное дуальное объявление и на C++ и на C. Т.е. "мораль" неверна.

P.S. Ваш ответ абсолютно верный, но не на тот вопрос... Скорее всего, предполагалось такое использование: кто-то использует C и хочет получить библиотеку функций — человек хочет написать её на C++, использовать будут приблизительно так: LoadLibrary(...) GetProcAddress(.."newobj")... GetProcAddress(.."obj_method1") (причём obj_method1 можно сразу отмапить на метод класса)
далее
void *obj=newobj();
obj_method1(obj, "hello");

и человек спрашивает, что будет если в obj передать что-то, может быть NULL. Мне представляется, что вопрос навеян именно этим.


Спасибо за дискуссию :)
Re[10]: ((SA*)0)->Func();
От: Alexander Shargin Россия RSDN.ru
Дата: 29.04.02 17:52
Оценка:
Здравствуйте Sergey, Вы писали:

S>Неправда ваша Если мне окончательно не отшибло память, то та версия MFC, что шла с VC 4.0, существовала не только для Win32, но и для Mac OS.


AS>>Она использует не только MS-specific код, но и ассемблерные вставки.

S>Ну дык реализация библиотеки для конкретной платформы могет делать что ей заблагорассудится. Сишная RTL от MS тоже кое-где на ассемблере писана, главное чтоб интерфейс был одинаковымю

Ты не понял. Я имел в виду, что MFC не может быз изменений компилироваться на другой платформе (и даже на другом компиляторе). При этом вполне возможно её портирование на другие платформы, которое не раз и делалось. Но это уже другая история. При наличии большого желания и средств можно запортить что угодно, не то что MFC.
--
Я думал, ты огромный страшный Бажище,
А ты недоучка, крохотный Бажик...
Re[6]: ((SA*)0)->Func();
От: DarkGray Россия http://blog.metatech.ru/post/ogni-razrabotki.aspx
Дата: 29.04.02 20:07
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Из приведенной тобой выдержки, кстати, может показаться, что к статическим членам класса можно доступаться через null-указатель. Это не так. Любое вычисляемое выражение, содержащее разыменование null-указателя порождает неопределенное поведение.


А в чем это "неопределенное поведение" может выражаться при обращении через нулевой указатель к статической функции?

((S*)0)->StaticFunc();
Re[7]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 29.04.02 20:41
Оценка:
Здравствуйте DarkGray, Вы писали:

DG>Здравствуйте Андрей Тарасевич, Вы писали:


АТ>>Из приведенной тобой выдержки, кстати, может показаться, что к статическим членам класса можно доступаться через null-указатель. Это не так. Любое вычисляемое выражение, содержащее разыменование null-указателя порождает неопределенное поведение.


DG>А в чем это "неопределенное поведение" может выражаться при обращении через нулевой указатель к статической функции?


DG>
((S*)0)->>StaticFunc();
DG>


А кто его знает. Буквально такая запись означает

*(S*) 0;
S::StaticFunc();


Неопределенное поведение вызывается именно первым выражением. Вызов статического метода тут, собственно, ни при чем. Если некоторый компилятор возьмется транслировать этот код буквально, то в ответ на первое выражение он может сгенерировать код, загружающий разыменовываемый указатель в некий выделенный адресный регистр. Вот тут-то система и рухнет.

Например, на современной платформе x86, при попытке загрузки неправильного значения в селекторный регистр (DS, ES и т.п.) программа падает сразу, а не при попытке собственно обращения к памяти через такой регистр.

Разумеется, большинство современных компиляторов проигнорирует такое выражение. Но стандарт языка этого не требует. Любой компилятор имеет право генерировать для выражения '*(S*) 0' падающий код.
Best regards,
Андрей Тарасевич
Re[6]: ((SA*)0)->Func();
От: Павел Кузнецов  
Дата: 30.04.02 05:15
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Стандарт языка подчеркивает, что выражение, стоящее слева от '.' или '->' при доступе к члену класса (методу или полю), обязательно вычисляется. (...) Вычисление этого выражения автоматически означает разыменование использованного в нем указателя.


Да, действительно. В основном я отвечал на часть "может написано, может не написано." В смысле, что таки да, написано.

АТ>А вот например параметр 'typeid' может содержать разыменованный null-указатель, т.к. выражение-параметр 'typeid' не вычисляется.


Вычисляется в случае полиморфных типов. Может, ты имел в виду sizeof?
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Re[11]: ((SA*)0)->Func();
От: Sergey Россия  
Дата: 30.04.02 05:54
Оценка:
Здравствуйте Alexander Shargin, Вы писали:

AS>Здравствуйте Sergey, Вы писали:


S>>Неправда ваша Если мне окончательно не отшибло память, то та версия MFC, что шла с VC 4.0, существовала не только для Win32, но и для Mac OS.


AS>>>Она использует не только MS-specific код, но и ассемблерные вставки.

S>>Ну дык реализация библиотеки для конкретной платформы могет делать что ей заблагорассудится. Сишная RTL от MS тоже кое-где на ассемблере писана, главное чтоб интерфейс был одинаковымю

AS>Ты не понял. Я имел в виду, что MFC не может быз изменений компилироваться на другой платформе (и даже на другом компиляторе). При этом вполне возможно её портирование на другие платформы, которое не раз и делалось. Но это уже другая история. При наличии большого желания и средств можно запортить что угодно, не то что MFC.


Ну, если фразу "MFC платформеннонезависимой никогда не планировалась. Она использует не только MS-specific код, но и ассемблерные вставки." следует понимать как "MFC не может быз изменений компилироваться на другой платформе (и даже на другом компиляторе). При этом вполне возможно её портирование на другие платформы, которое не раз и делалось." то да — я этого не понял. Кстати, про другой компилятор я ничего не говорил
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Re[6]: ((SA*)0)->Func();
От: VVV Россия  
Дата: 30.04.02 11:40
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Здравствуйте Павел Кузнецов, Вы писали:


АТ>>>Может написано, может не написано. Это, в принципе, неважно. Стандарт вместо этого совершенно глухо запрещает все возможные пути падания внутрь метода объекта через null-указатель. А именно: запрещается разадресовывать null-указатель. Это автоматически означает, что, пока поведение программы определено, внутри метода объекта this никак не может быть равен null-указателю.


ПК>>AFAIK, вызов метода сам по себе не подразумевает разыменования указателя. Поэтому в стандарте (9.3.1/1) явно указано, что "If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined". Т.к. null-pointer гарантированно не является указателем ни на какой объект, это предложение включает и вызов метода через null-pointer.


АТ>Я не согласен. Возможность вызова метода класса (причем как статического, так и нестатического) через null-указатель перекрыта стандартом еще раньше. Стандарт языка подчеркивает, что выражение, стоящее слева от '.' или '->' при доступе к члену класса (методу или полю), обязательно вычисляется. Причем оно вычисляется независимо от того, является ли доступаемый член класса статическим или нет. Вычисление этого выражения автоматически означает разыменование использованного в нем указателя. Разыменование null-указателя порождает неопределенное поведение.


АТ>Из приведенной тобой выдержки, кстати, может показаться, что к статическим членам класса можно доступаться через null-указатель. Это не так. Любое вычисляемое выражение, содержащее разыменование null-указателя порождает неопределенное поведение. А вот например параметр 'typeid' может содержать разыменованный null-указатель, т.к. выражение-параметр 'typeid' не вычисляется.


АТ>Также можно заметить, что если бы вызов метода не подразумевал разыменования указателя, то стандарт языка С++ должен был бы содержать отдельное замечание по поводу вызова виртуальных методов. Эти последние вызывать через null-указатель все таки нельзя (мы все в глубине души знаем, что такой вызов существенно подразумевает разыменование указателя). Я не нашел в стандарте такого замечания.


Вычисление адреса(указателя) совсем не то же самое, что и доступ(разыменование) по этому адресу. Поэтому в языке есть специальные операторы -> и * для разыменования указателей, компилятор не обращается к данным без прямого указания программиста. Что значит у Вас выражение "Вычисление этого выражения автоматически означает разыменование использованного в нем указателя"??? Разыменование — это есть способ получение данных по адресу, в Вашем выражении сколько данных будет "автоматически" получать компилятор?? 1 байт? 2 байта? или... сколько и куда???

Не надо забывать, что языки, такие как C и C++ используются для написания программ не только на PC но и для различных устройств(типа мобильных телефонов), а там организация памяти может быть абсолютно любой (т.е. по NULL адресу могут находиться системные структуры). К тому же, по Вашему заявлению, получается, что корректно заполнить таблицу прерываний с помощью C++ нельзя, ибо начинается эта таблица по адресу 0? Т.е. специально для этого надо делать ассемблерные вставки? Да, NULL (он же 0) — особое число и его можно сравнивать с указателями без приведения типа и можно сигнализировать об ошибках выделения памяти(на PC), но не более того.

пример корректного применения NULL указателя:
#include "stdafx.h"

class Mem{
public:
    unsigned char m[0x400000];
    unsigned char prg[256];
    void Print()
    {
        for(int i=0; i < 256; i++)
            printf("%c", prg[i] >= (unsigned char)' ' ? prg[i] : '.');
        printf("\n");
    }
};


int main(int argc, char* argv[])
{
    Mem *pm=(Mem*)NULL;
    
    for(int i=0; i < 256; i++)
        printf("%c", pm->prg[i] >= (unsigned char)' ' ? pm->prg[i] : '.');
    printf("\n");
    
    pm->Print();

    return 0;
}


если бы Вы привели конкретное место в стандарте (хотя бы ссылку) может быть этой дискуссии и не было бы :)
стандарт о ((SA*)0)->Func();
От: Павел Кузнецов  
Дата: 30.04.02 12:16
Оценка:
#Имя: FAQ.cpp.*nullptr.std
Здравствуйте VVV, Вы писали:

DG>Должен ли по стандарту работать следующий код?


struct SA
{
  void Func() //невиртуальная
  {
    if (this == NULL) 
      return;
  }  
  static void StaticFunc() {}
};

void main()
{
  ((SA*)0)->StaticFunc(); //ИМХО, тут должно быть все нормально
  ((SA*)0)->Func(); //а вот здесь не понятно, мало ли чего может компилятор напихать
}

VVV>если бы Вы привели конкретное место в стандарте (хотя бы ссылку) может быть этой дискуссии и не было бы

Андрей прав. Закрываем дискуссию, вот соответствующие выдержки из стандарта:

5.2.5 Class member access (expr.ref)
1 A postfix expression followed by a dot . or an arrow ­->, optionally followed by the keyword template (14.8.1), and then followed by an idexpression, is a postfix expression. The postfix expression before the dot or arrow is evaluated; 58) (...)
58) This evaluation happens even if the result is unnecessary to determine the value of the entire postfix expression, for example if the idexpression denotes a static member.
3 If E1 has the type “pointer to class X,” then the expression E1->E2 is converted to the equivalent form (*(E1)).E2; (...)

1.9 Program execution (intro.execution)
4 Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer).

8.3.2 References (dcl.ref)
4 (...) [Note: in particular, a null reference cannot exist in a welldefined program, because the only way to create such a reference would be to bind it to the “object” obtained by dereferencing a null pointer, which causes undefined behavior. (...)
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Re[7]: ((SA*)0)->Func();
От: Андрей Тарасевич Беларусь  
Дата: 30.04.02 15:56
Оценка:
Здравствуйте VVV, Вы писали:

VVV>Вычисление адреса(указателя) совсем не то же самое, что и доступ(разыменование) по этому адресу. Поэтому в языке есть специальные операторы -> и * для разыменования указателей, компилятор не обращается к данным без прямого указания программиста. Что значит у Вас выражение "Вычисление этого выражения автоматически означает разыменование использованного в нем указателя"??? Разыменование — это есть способ получение данных по адресу, в Вашем выражении сколько данных будет "автоматически" получать компилятор?? 1 байт? 2 байта? или... сколько и куда???


Нет. Разыменованием в С++ называется применение к указателю операторов '*' или '->'. Получаются ли кем-то указуемые данные или не получаются — неважно. Если есть применение '*' и '->' в вычисляемом выражении — згачит есть разыменование.

VVV>Не надо забывать, что языки, такие как C и C++ используются для написания программ не только на PC но и для различных устройств(типа мобильных телефонов), а там организация памяти может быть абсолютно любой (т.е. по NULL адресу могут находиться системные структуры).


Конкретные компиляторы выбирают значение null-указателя таким, чтобы он не указывал ни ни какие данные, к которым может понадобится доступ из программы. Если на некотором устройстве по адресу 0x0000 сидят какие-то данные, с которыми нужно уметь работать из С/С++ программ, то в компиляторе С/С++ для этого устройства значение null-указателя будет не 0x0000, а каким-то другим, например, 0xFFFF.

Не забывай одной важного факта: то, что null-указатель получается путем преобразования целочисленной константы 0 к типу "указатель", совсем не означает, что битовое представление null-указателя стостоит из одних нулей. Т.е. это совсем не означает, что null-указатель — это указатель на адрес 0x0000. Null-указатель может представляться абсолютно любым физическим адресом. Каким — зависит от платформы.

VVV>К тому же, по Вашему заявлению, получается, что корректно заполнить таблицу прерываний с помощью C++ нельзя, ибо начинается эта таблица по адресу 0? Т.е. специально для этого надо делать ассемблерные вставки? Да, NULL (он же 0) — особое число и его можно сравнивать с указателями без приведения типа и можно сигнализировать об ошибках выделения памяти(на PC), но не более того.


Также хочу напомнить, что речь здесь идет о языках С/С++, как они описаны в своей спецификации. Разнообразные платформенно-зависимые расширения никаким боком к этой дискусии не относятся.

Если на конкретной платформе разыменование null-указателя не приводит к проблемам или даже используется для выполнения каких-то полезных дейсивий — флаг ей в руки. Но к корректному С/С++ это никаким боком относится не будет. Точнее, это вообще никаким боком к С/С++ относится не будет.

VVV>пример корректного применения NULL указателя:


С точки зрения С/С++ программа прождает неопределенное поведение, т.к. содержит разыменование null-указателя. Да и еще масса проблем (с точки зрения С/С++) в этой программе присутствует.

VVV>если бы Вы привели конкретное место в стандарте (хотя бы ссылку) может быть этой дискуссии и не было бы


Хе, хе... Что-то мне подсказывает, что приведение выдержек из стандарта дискусси не остановит. Anyway, см. письмо Павла рядом.
Best regards,
Андрей Тарасевич
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.