1

У меня есть довольно большой проект с несколькими интерфейсами и реализациями. Код был реализован в среде linux с использованием g ++ (5.4, я думаю). После переноса кода в Windows и запуска его с помощью VS15 (MSVC v140) я получил нарушение прав доступа после попытки доступа к литому указателю.Нарушение прав доступа C++ в msvc, но не gcc для множественного наследования и литья

Это иерархия наследования в коде ниже:

    A  
       /\ 
    virtual / \ 
      / | 
      B  | 
      |  | virtual 
      C  | 
      |  | 
      \ /
       \ /
       D 

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

Следующие работает с НКУ и печатает foo called дважды (Live demo on rextester), но с MSVC на втором вызове foo аварий с нарушением доступа (Live demo on rextester)

#include <iostream> 

class A{}; 

class B : public virtual A{}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B** ext) 
    { 
     if (id == 1) 
     { 
      *ext = (C*)this; 
      return true; 
     } 

     if (id == 42) 
     { 
      C** pp_ext = (C**)(ext); 
      *pp_ext = (C*)this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 


int main() 
{ 
    D s; 
    C* foo_ext = nullptr; 
    s.convert(42, (B**)&foo_ext); 
    foo_ext->foo(); 

    foo_ext = nullptr; 
    s.convert(1, (B**)&foo_ext); 
    foo_ext->foo(); 

    return 0; 
} 

Первый - Я пропускаю фундаментальную ошибку в *ext = (C*)this; преобразования?

Второй - Почему этот код отличается от двух компиляторов?

EDIT:

  1. Этот код использует указатели, указатели на указатели и построен с этим наследование по уважительным причинам (одна из которых является ABI совместимый интерфейс).

  2. dynamic_cast не изменяет поведение в этом случае.

  3. Если я позвоню static_cast<C*>(*ext)->foo(); после *ext = (C*)this;, он вызовет foo, но не будет возвращен с convert. Это то, что я уже понял, и это заставило меня понять, что решение для 42 является (хорошим?) Решением.

+3

Прекратить использование c-style cast и слишком много звезд. – LogicStuff

+1

http://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used – LogicStuff

+0

И это не повод для голосования вопрос. – ZivS

ответ

2

Имея дело с наследованием, и особенно с множественным наследованием, вы должны действительно стараться избегать приведения целиком. Но если вам нужно бросить, используйте либо static_cast, либо dynamic_cast. Таким образом, компилятор поможет избежать недопустимых преобразований. Если вы делаете что-то еще, тогда вам нужно понять все детали C++, а также лучше или лучше самого компилятора! В противном случае вы рискуете легко ошибиться. Как вы здесь.

Попробуйте изменить ваш главный на что-то вроде:

int main() 
{ 
    D s; 

    A* a = &s; 
    B* b = &s; 
    C* c = &s; 
    std::cout << "A address = " << a << std::endl; 
    std::cout << "B address = " << b << std::endl; 
    std::cout << "C address = " << c << std::endl; 
    std::cout << "D address = " << &s << std::endl; 

    C* foo_ext = nullptr; 
    s.convert(42, (B**)&foo_ext); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    foo_ext = nullptr; 
    s.convert(1, (B**)&foo_ext); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    return 0; 
} 

Бег, что до аварии, я получаю:

A address = 0037FEA0 
B address = 0037FE9C 
C address = 0037FE98 
D address = 0037FE98 
foo_ext = 0037FE98 
foo called 
foo_ext = 0037FE9C 

Очевидно во втором случае foo_ext не быть установлен в надлежащем C адрес объекта, а вместо этого - часть B. На практике тогда вызов foo() может проходить через неправильный или несуществующий указатель виртуальной таблицы, что приводит к сбою.

Теперь, почему первый случай работает?Ну, сокращая его до минимума, что происходит, вы эффективно сделать:

C* foo_ext = nullptr; 
C** ppc = &foo_ext; 
B** ppb = (B**)ppc; 
C** pp_ext = (C**)ppb; 
*pp_ext = &s; 

Так начинается с C указателя и заканчивается с C указателями. И компилятор знает, как правильно сдвинуть указатель D на указатель C. (.. Производная базовой конверсии Компилятор не нужен бросок, чтобы сделать это)

Однако во втором случае вы фактически имеете:

C* foo_ext = nullptr; 
C** ppc = &foo_ext; 
B** ext = (B**)ppc; 
*ext = &s; 

Так что в последней строке компилятор сдвигая D указатель, чтобы стать указателем B. Но на самом деле этот адрес заканчивается в указателе C. Таким образом, он слишком сильно переместился в иерархию наследования!


Теперь, как исправить вам программу ... Ну, основная идея, конечно, избавиться от большей части отливки и заменить оставшиеся пару пятен со статическими или динамическими слепками. Попробуйте следующее:

#include <iostream> 

class A {}; 

class B : public virtual A {}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B** ext) 
    { 
     if (id == 1) 
     { 
      *ext = this; 
      return true; 
     } 

     if (id == 42) 
     { 
      *ext = this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 


int main() 
{ 
    D s; 

    A* a = &s; 
    B* b = &s; 
    C* c = &s; 
    std::cout << "A address = " << a << std::endl; 
    std::cout << "B address = " << b << std::endl; 
    std::cout << "C address = " << c << std::endl; 
    std::cout << "D address = " << &s << std::endl; 

    B* b_pointer = nullptr; 
    C* foo_ext = nullptr; 
    s.convert(42, &b_pointer); 
    foo_ext = static_cast<C*>(b_pointer); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    b_pointer = nullptr; 
    foo_ext = nullptr; 
    s.convert(1, &b_pointer); 
    foo_ext = static_cast<C*>(b_pointer); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    return 0; 
} 

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

Я предполагаю, что этот пример был основан на проблеме реального мира. Я не могу сказать, подходит ли это точное решение для этого (поскольку, очевидно, я не знаю реальной ситуации). Но принцип звучит: не лгите компилятору, вызывая сомнительные приведения. Пусть он автоматически конвертирует, когда это возможно, и прибегает только к static_cast или dynamic_cast, когда это необходимо.

+0

Благодарим вас за подробный ответ, я также воспроизвел это поведение без множественного наследования, так как реальной проблемой является приведение к B ** перед вызовом. Есть ли у вас какие-либо идеи, что вызывает разницу в компиляторах? – ZivS

+0

@ZivS Различные компиляторы могут делать разные варианты расположения макетов частей объекта. Наличие классов без членов, в частности, может позволить оптимизацию, когда базовая часть объекта не занимает какое-либо пространство в пределах общего размера производного объекта. Таким образом, в других работах указатели на производные и базы заканчиваются тем же адресом. Обратите внимание, что в моих результатах есть экземпляр с адресами C и D. Может быть, на g ++, который случается для B и C, и поэтому ваш код работает, несмотря на плохой актерский состав. Я бы не хотел на это полагаться. – TheUndeadFish

+0

@ZivS Где у вас MI в этом коде? – curiousguy

1

Первая проблема выполнения приходит на:

s.convert(42, (B**)&foo_ext); 

foo_ext имеет тип C *. Таким образом, использование *ext внутри convert совершает строгие нарушения псевдонимов, обращаясь к памяти C *, как если бы это был B *. В общем, указатели на разные типы могут иметь разный размер и представление; но даже если они этого не делают, им все равно не разрешается использовать их.

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

MSVC, вероятно, не обеспечивает строгого сглаживания в этой ситуации (на самом деле некоторые вещи в Windows API полагаются на это поведение). Но если вы хотите написать переносимый код, было бы неплохо не полагаться на строгие нарушения псевдонимов.


convert функция использует передачу по указателю. В C++ никогда не бывает необходимости. Вместо этого вы можете использовать pass-by-reference в качестве языковой функции. Это уменьшает вероятность ошибок - некоторые ошибки в вашем коде фактически не могут быть выражены в ссылочной нотации.

Вот модифицированная версия:

#include <iostream> 

class A{}; 

class B : public virtual A{}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B*& ext) 
    { 
     if (id == 1) 
     { 
      ext = static_cast<C *>(this); // note: redundant cast 
      return true; 
     } 

     if (id == 42) 
     { 
      ext = this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 

int main() 
{ 
    D s; 
    B* foo_ext_b; 
    C* foo_ext; 

    foo_ext_b = nullptr; 

    if (!s.convert(42, foo_ext_b)) 
     throw std::runtime_error("convert42 failed"); 

    foo_ext = static_cast<C *>(foo_ext_b); 
    foo_ext->foo(); 

    foo_ext_b = nullptr; 

    if (!s.convert(1, foo_ext_b)) 
     throw std::runtime_error("convert1 failed"); 

    foo_ext = static_cast<C *>(foo_ext_b); 
    foo_ext->foo(); 

    return 0; 
} 

Обратите внимание, что при использовании foo_ext = static_cast<C *>(foo_ext_b);, в общем, к ошибкам. Будет тихое неопределенное поведение, если convert «возвратил» B *, который не указывал на B, который является базовым классом экземпляра C.

Для безопасности вы должны использовать вместо этого dynamic_cast. Но чтобы позволить dynamic_cast работать, базовый класс должен иметь хотя бы одну виртуальную функцию. Вы можете добавить виртуальный деструктор в B или A.

+0

Указатели на классы должны иметь одинаковый размер. – curiousguy