Неопределенное поведение с const_cast - PullRequest
9 голосов
/ 08 сентября 2011

Я надеялся, что кто-то сможет прояснить, что именно подразумевается под неопределенным поведением в C ++.Учитывая следующее определение класса:

class Foo
{
public:
    explicit Foo(int Value): m_Int(Value) { }
    void SetValue(int Value) { m_Int = Value; }

private:
    Foo(const Foo& rhs);
    const Foo& operator=(const Foo& rhs);

private:
    int m_Int;
};

Если я правильно понял, два const_casts и для ссылки, и для указателя в следующем коде будут удалять константу исходного объекта типа Foo, нолюбые попытки изменить этот объект с помощью либо указателя, либо ссылки приведут к неопределенному поведению.

int main()
{
    const Foo MyConstFoo(0);
    Foo& rFoo = const_cast<Foo&>(MyConstFoo);
    Foo* pFoo = const_cast<Foo*>(&MyConstFoo);

    //MyConstFoo.SetValue(1);   //Error as MyConstFoo is const
    rFoo.SetValue(2);           //Undefined behaviour
    pFoo->SetValue(3);          //Undefined behaviour

    return 0;
}

Меня удивляет то, почему это работает и будет изменять исходный объект const, но недаже предупредить меня, чтобы уведомить меня, что это поведение не определено.Я знаю, что const_casts, в общем и целом, осуждают, но я могу представить себе случай, когда недостаток осведомленности о том, что приведение в стиле C может привести к созданию const_cast, может произойти незамеченным, например:

Foo& rAnotherFoo = (Foo&)MyConstFoo;
Foo* pAnotherFoo = (Foo*)&MyConstFoo;

rAnotherFoo->SetValue(4);
pAnotherFoo->SetValue(5);

При каких обстоятельствах такое поведение может привести к фатальной ошибке во время выполнения?Есть ли какие-то настройки компилятора, которые я могу установить, чтобы предупредить меня об этом (потенциально) опасном поведении?

Примечание: я использую MSVC2008.

Ответы [ 7 ]

11 голосов
/ 08 сентября 2011

Я надеялся, что кто-то сможет прояснить, что именно подразумевается под неопределенным поведением в C ++.

Технически, «неопределенное поведение» означает, что язык не определяет семантику для таких действий.

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

Что меня озадачивает, так это , почему это работает и изменяет исходный объект const , но даже не выдает предупреждения с предупреждением, что это поведение не определено.

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

Факторы, которые могут привести к этому, просто слишком сложны, чтобы их перечислять. Рассмотрим случай разыменования неинициализированного указателя (также UB): «объект», с которым вы затем работаете, будет иметь некоторый произвольный адрес памяти, который зависит от того, какое значение было в памяти в месте расположения указателя; что «значение» потенциально зависит от предыдущих вызовов программы, предыдущей работы в той же программе, хранения предоставленного пользователем ввода и т. д. Попытка рационализировать возможные результаты вызова Undefined Behavior просто невозможна, поэтому, опять же, мы обычно не вместо этого просто скажите " не делайте этого ".

Что меня озадачивает, так это то, что это работает и изменяет исходный объект const , но даже не выдает предупреждение с предупреждением, что это поведение не определено.

В качестве дополнительного усложнения компиляторам не требуется для диагностики (выдачи предупреждений / ошибок) для неопределенного поведения, поскольку код, который вызывает неопределенное поведение, не совпадает с кодом, который сформирован неправильно (то есть явно незаконный). Во многих случаях компилятор не может даже обнаружить UB, поэтому это область, в которой программист несет ответственность за правильное написание кода.

Система типов & mdash; включая существование и семантику ключевого слова const & mdash; обеспечивает базовую защиту от написания кода, который может сломаться; программист C ++ всегда должен помнить, что это подрывает систему & mdash; например взломав const ness & mdash; делается на свой страх и риск и, как правило, является плохой идеей. ™

Я могу представить себе случай, когда неосведомленность о том, что приведение в стиле C может привести к созданию const_cast, может произойти незамеченной.

Абсолютно. С достаточно высокими уровнями предупреждений здравомыслящий компилятор может выбрать, чтобы предупредить вас об этом, но это не обязательно, а может и нет. В общем, это хорошая причина, по которой бросают вызовы в стиле C, но они все еще поддерживаются для обратной совместимости с C. Это просто одна из тех неудачных вещей.

4 голосов
/ 08 марта 2014

Неопределенное поведение зависит от того, как родился объект , вы можете увидеть Стефан , объясняя это примерно в 00:10:00, но по сути следуйте приведенному ниже коду:

void f(int const &arg)
{
    int &danger( const_cast<int&>(arg); 
    danger = 23; // When is this UB?
}

Теперь есть два случая для звонка f

int K(1);
f(k); // OK
const int AK(1); 
f(AK); // triggers undefined behaviour

Подводя итог, K был рожден неконстантным, так что приведение в порядке при вызове f, тогда как AK было рождено const, так что ... UB это так.

4 голосов
/ 08 сентября 2011

Что меня озадачивает, так это то, что это работает

Вот что означает неопределенное поведение.
Он может делать все, что угодно, даже если кажется, что он работает.
Если увеличитьВаш уровень оптимизации до его максимального значения, вероятно, перестанет работать.

, но даже не выдает мне предупреждение с предупреждением, что это поведение не определено.

В момент, когда он выполняет модификацию, объект является , а не const.В общем случае он не может сказать, что объект изначально был const, поэтому предупредить вас невозможно.Даже если это было каждое утверждение оценивается само по себе, без ссылки на другие (при рассмотрении такого рода генерации предупреждений).

Во-вторых, используя приведение, вы сообщаете компилятору "Я знаю, что яЯ отменяю все ваши функции безопасности и просто делаю это ".

Например, следующее работает очень хорошо: (или будет казаться слишком (по типу назального деамона))

float aFloat;

int& anIntRef = (int&)aFloat;  // I know what I am doing ignore the fact that this is sensable
int* anIntPtr = (int*)&aFloat;

anIntRef  = 12;
*anIntPtr = 13;

Я знаю, что const_casts, в широком смысле,хмурится

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

но я могу представить себе случай, когда неосведомленность о том, что приведение в стиле C может привести к созданию const_cast, может произойти незамеченным, например:

В C ++ нет необходимостииспользовать приведение в стиле C.
В худшем случае приведение в C-стиле может быть заменено reinterpret_cast <>, но при переносе кода вы хотите увидеть, могли ли вы использовать static_cast <>.Смысл приведений C ++ состоит в том, чтобы выделить их , чтобы вы могли их видеть и сразу увидеть разницу между опасными бросками и доброкачественными.

4 голосов
/ 08 сентября 2011

Неопределенное поведение буквально означает только это: поведение, которое не определено языковым стандартом.Обычно это происходит в ситуациях, когда код делает что-то не так, но ошибка не может быть обнаружена компилятором.Единственный способ отловить ошибку - ввести тест во время выполнения, который снизит производительность.Таким образом, вместо этого, спецификация языка говорит вам, что вы не должны делать определенные вещи, и, если вы это сделаете, тогда может произойти что-нибудь.При проверке во время компиляции существует три вероятных сценария:

  • он обрабатывается как непостоянный объект, и запись в него изменяет его;
  • он помещается в запись-защищенная память, и запись в нее вызывает ошибку защиты;
  • она заменяется (во время оптимизации) постоянными значениями, встроенными в скомпилированный код, поэтому после записи в нее она все равно будет иметь свое начальное значение.

В вашем тесте вы оказались в первом сценарии - объект (почти наверняка) создан в стеке, который не защищен от записи.Вы можете обнаружить, что вы получаете второй сценарий, если объект статический, и третий, если вы включаете дополнительную оптимизацию.

Как правило, компилятор не может диагностировать эту ошибку - нет способа сообщить (кромев очень простых примерах, подобных вашему), является ли цель ссылки или указателя постоянной или нет.Вы должны убедиться, что вы используете const_cast только тогда, когда можете гарантировать его безопасность - либо когда объект не является постоянным, либо когда вы на самом деле не собираетесь его изменять.

2 голосов
/ 08 сентября 2011

Классическим примером будет попытка изменить строковый литерал const, который может существовать в защищенном сегменте данных.

0 голосов
/ 08 сентября 2011

Статические и постоянные данные часто хранятся в другой части вашей программы, чем локальные переменные. Для константных переменных эти области часто находятся в режиме только для чтения, чтобы обеспечить константность переменных. Попытка записи в постоянную память приводит к «неопределенному поведению», потому что реакция зависит от вашей операционной системы. «Неопределенное поведение» означает, что язык не определяет, как должен обрабатываться этот случай.

Если вы хотите более подробное объяснение памяти, я предлагаю вам прочитать это . Это объяснение, основанное на UNIX, но похожий механизм используется во всех ОС.

0 голосов
/ 08 сентября 2011

Компиляторы могут помещать постоянные данные в части памяти, доступные только для чтения, для оптимизации, и попытка изменить эти данные приведет к UB.

...