Почему странное поведение с возвратом указателя на исходный класс? - PullRequest
6 голосов
/ 11 июня 2011

Предположим, что в моем коде я должен сохранить void* в качестве элемента данных и при необходимости привести его обратно к исходному указателю class. Чтобы проверить его надежность, я написал тестовую программу (linux ubuntu 4.4.1 g ++ -04 -Wall) и был шокирован, увидев поведение.

struct A
{
  int i;
  static int c;
  A () : i(c++) { cout<<"A() : i("<<i<<")\n"; }
};
int A::c;

int main ()
{
  void *p = new A[3];  // good behavior for A* p = new A[3];
  cout<<"p->i = "<<((A*)p)->i<<endl;
  ((A*&)p)++;
  cout<<"p->i = "<<((A*)p)->i<<endl;
  ((A*&)p)++;
  cout<<"p->i = "<<((A*)p)->i<<endl;
}

Это всего лишь тестовая программа; в моем случае обязательно хранить любой указатель как void* и затем приводить его обратно к фактическому указателю (с помощью template). Так что давайте не будем беспокоиться об этой части. Выход вышеуказанного кода равен

p->i = 0
p->i = 0 // ?? why not 1
p->i = 1

Однако, если вы измените void* p; на A* p;, это даст ожидаемое поведение . ПОЧЕМУ?

Еще один вопрос, мне не сойдет с рук (A*&), иначе я не смогу использовать operator ++; но он также выдает предупреждение, так как разыменование указателя типа-наказанного нарушит правила строгого псевдонима . Есть ли какой-нибудь приличный способ преодолеть предупреждение?

Ответы [ 3 ]

11 голосов
/ 11 июня 2011

Итак, как предупреждает вас компилятор, вы нарушаете строгое правило псевдонимов, которое формально означает, что результаты не определены.

Вы можете устранить строгое нарушение псевдонимов, используя шаблон функции для приращения:

template<typename T>
void advance_pointer_as(void*& p, int n = 1) {
    T* p_a(static_cast<T*>(p));
    p_a += n;
    p = p_a;
}

С этим шаблоном функции следующее определение main() дает ожидаемые результаты на компиляторе Ideone (и не выдает предупреждений):

int main()
{
    void* p = new A[3];
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
    advance_pointer_as<A>(p);
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
    advance_pointer_as<A>(p);
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
}
6 голосов
/ 11 июня 2011

Вы уже получили правильный ответ, и именно нарушение строгого правила псевдонимов приводит к непредсказуемому поведению кода.Я просто хотел бы отметить, что заголовок вашего вопроса ссылается на «приведение указателя на исходный класс».На самом деле ваш код не имеет ничего общего с приведением «назад».Ваш код выполняет реинтерпретацию необработанного содержимого памяти, занятого указателем void * в качестве указателя A *.Это не «отбрасывание».Это реинтерпретация .Даже отдаленно одно и то же.

Хороший способ проиллюстрировать разницу - использовать примеры int и float.Значение float, объявленное и инициализированное как

float f = 2.0;

, может быть приведено (явно или неявно преобразовано ) к int типу

int i = (int) f;

с ожидаемымрезультат

assert(i == 2);

Это действительно приведение (преобразование).

В качестве альтернативы, то же значение float можно также интерпретировать как значение int

int i = (int &) f;

Однако в этом случае значение i будет совершенно бессмысленным и обычнонепредсказуемы.Я надеюсь, что из этих примеров легко увидеть разницу между преобразованием и реинтерпретацией памяти.

Реинтерпретация - это именно то, что вы делаете в своем коде.Выражение (A *&) p представляет собой не что иное, как реинтерпретацию необработанной памяти, занятой указателем void *p в качестве указателя типа A *.Язык не гарантирует, что эти два типа указателей имеют одинаковое представление и даже одинаковый размер.Таким образом, ожидание предсказуемого поведения в вашем коде похоже на ожидание, что приведенное выше выражение (int &) f оценивается как 2.

Правильный способ действительно "отбросить" указатель void * - это сделать (A *) p, а не (A *&) p.Результатом (A *) p действительно будет исходное значение указателя, которым можно безопасно манипулировать с помощью арифметики указателя.Единственный правильный способ получить исходное значение как lvalue - это использовать дополнительную переменную

A *pa = (A *) p;
...
pa++;
...

. И нет никакого законного способа создать lvalue «на месте», как вы пытались сделать с помощью * 1048.* бросать.Поведение вашего кода является иллюстрацией этого.

2 голосов
/ 11 июня 2011

Как прокомментировали другие, ваш код выглядит так, как будто он должен работать.Только один раз (за 17 с лишним лет программирования на C ++) я наткнулся на что-то, где я смотрел прямо на код, и поведение, как в вашем случае, просто не имело смысла.В итоге я запустил код через отладчик и открыл окно разборки.Я нашел то, что можно объяснить только как ошибку в компиляторе VS2003, потому что в нем отсутствовала ровно одна инструкция.Простая перестановка локальных переменных в верхней части функции (примерно 30 строк от ошибки) заставила компилятор вернуть правильную инструкцию. Так что попробуйте отладчик с разборкой и следуйте памяти / регистрам, чтобы увидеть, что он на самом деле делает?

Что касается продвижения указателя, вы должны быть в состоянии продвинуть его, выполнив:

p = (char*)p + sizeof( A );

VS2003 - VS2010 никогда не будет на вас жаловаться, не уверен насчет g ++

...