Почему запись в неконстантный объект после отбрасывания const указателя на этот объект не UB? - PullRequest
6 голосов
/ 16 декабря 2011

В соответствии со Стандартом C ++ можно отбросить const из указателя и записать объект, если объект изначально не является const.Так что это:

 const Type* object = new Type();
 const_cast<Type*>( object )->Modify();

нормально, но это:

 const Type object;
 const_cast<Type*>( &object )->Modify();

это UB.

Причина в том, что когда сам объект имеет значение const, компилятору разрешено оптимизировать доступ к нему, например, не выполнять повторные чтения, поскольку повторные чтения не имеют смысла для объектаэто не изменится.

Вопрос в том, как компилятор узнает, какие объекты на самом деле const?Например, у меня есть функция:

void function( const Type* object )
{
    const_cast<Type*>( object )->Modify();
}

, и она скомпилирована в статическую библиотеку, и компилятор не знает, для каких объектов она будет вызываться.

Теперь вызывающий код можетсделайте это:

Type* object = new Type();
function( object );

и все будет в порядке, или он может сделать это:

const Type object;
function( &object );

и это будет неопределенное поведение.

Как предполагается компиляторпридерживаться таких требований?Как предполагается, что первый работает, а второй не работает?

Ответы [ 5 ]

6 голосов
/ 16 декабря 2011

Когда вы говорите: «Как предполагается, что первый работает, а второй не работает?»реализация требуется только для того, чтобы заставить первый работать, не нужно - если только он не хочет помочь программисту - приложить дополнительные усилия, чтобы заставить последний не работать каким-то конкретным образом. неопределенное поведение дает свободу реализации, а не обязательство.

Возьмем более конкретный пример.В этом примере в f() компилятор может установить возвращаемое значение равным 10, прежде чем он вызовет EvilMutate, потому что cobj.member является константой, как только конструктор cobj завершен и может впоследствии не быть записан.Он не может сделать то же самое предположение в g(), даже если вызывается только функция const.Если EvilMutate пытается изменить member при вызове cobj в f() происходит неопределенное поведение , и реализация не должна заставлять какие-либо последующие действия иметь какой-либо конкретный эффект.

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

struct Type {
    int member;
    void Mutate();
    void EvilMutate() const;
    Type() : member(10) {}
};


int f()
{
    const Type cobj;
    cobj.EvilMutate();
    return cobj.member; 
}

int g()
{
     Type obj;
     obj.EvilMutate();
     return obj.member; 
}
3 голосов
/ 16 декабря 2011

Компилятор может выполнять оптимизацию только для константных объектов, а не для ссылок / указателей на константные объекты (см. этот вопрос ). В вашем примере компилятор не может оптимизировать function, но он может оптимизировать код, используя const Type. Так как компилятор считает этот объект постоянным, изменение его (путем вызова function) может сделать что угодно, в том числе сбой вашей программы (например, если объект хранится в постоянной памяти) или работает как неконстантный версия (если модификация не мешает оптимизации)

Неконстантная версия не имеет проблем и отлично определена, вы просто модифицируете неконстантный объект, чтобы все было хорошо.

2 голосов
/ 23 июля 2015

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

Кроме того, если компилятор может сказать, что объект const всегда будет содержать определенную последовательность байтов, он может сообщить об этом компоновщику и позволить компоновщику увидеть, происходит ли эта последовательность байтов. где-нибудь в коде и, если это так, рассматривайте адрес объекта const как местоположение этой последовательности байтов (соблюдение различных ограничений для различных объектов, имеющих уникальные адреса, может быть немного сложнее, но это будет допустимо) , Если компилятор сказал компоновщику, что const char[4] всегда должен содержать последовательность байтов, которые, как оказалось, появляются в скомпилированном коде для некоторой функции, компоновщик может назначить этой переменной адрес в коде, где появляется эта последовательность байтов. Если const никогда не записывалось, такое поведение спасло бы четыре байта, но запись в const произвольно изменила бы значение другого кода.

Если запись в объект после отбрасывания const всегда была UB, способность отбрасывать константу была бы не очень полезна. Как таковая, способность часто играет роль в ситуациях, когда часть кода удерживает указатели - некоторые из которых const, а некоторые из них необходимо будет написать - в пользу другого кода . Если при отбрасывании константности const указателей на объекты, не являющиеся const, не определено поведение, код, содержащий указатели, должен знать, какие указатели являются const, а какие нужно записать , Однако, поскольку приведение к константам разрешено, для кода, содержащего указатели, достаточно объявить их все как const, а для кода, который знает, что указатель идентифицирует неконстантный объект и хочет его записать, для его приведения на не приведенный указатель.

Возможно, было бы полезно, если бы в C ++ были формы квалификаторов constvolatile), которые можно использовать в указателях для указания компилятору, что он может (или, в случае volatile, должен) учитывать указатель, идентифицирующий const и / или volatile объект , даже если компилятор знает, что это объект, и знает, что он не const и / или не объявлен volatile , Первый позволил бы компилятору предположить, что объект, идентифицированный указателем, не изменится в течение времени существования указателя, и кешировать данные на его основе; последний учел бы случаи, когда переменная может нуждаться в поддержке volatile доступа в некоторых редких ситуациях (обычно при запуске программы), но когда компилятор должен иметь возможность кэшировать свое значение после этого. Однако я не знаю предложений по добавлению таких функций.

1 голос
/ 16 декабря 2011

Неопределенное поведение означает Неопределенное поведение .Спецификация не дает никаких гарантий, что произойдет.

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

Но только то, что вы находитесь за пределами карты, не означает, что вы столкнетесь с драконом.Может быть, это будет пушистый кролик.

Думайте об этом так:

class BaseClass {};
class Derived : public BaseClass {};

BaseClass *pDerived = new Derived();
BaseClass *pBase = new Base();

Derived *pLegal = static_cast<Derived*>(pDerived);
Derived *pIllegal = static_cast<Derived*>(pBase);

C ++ определяет одно из этих приведений как совершенно допустимое.Другое приводит к неопределенному поведению.Означает ли это, что компилятор C ++ на самом деле проверяет тип и переключает переключатель «неопределенное поведение»?

Это означает, что компилятор C ++ более чем вероятно примет , что pBase на самом деле Derived и, следовательно, выполнит арифметику указателя, необходимую для преобразования pBase вDerived*.Если оно не на самом деле Derived, то вы получите неопределенные результаты.

Эта арифметика указателей может фактически быть невозможной;это может ничего не делать.Или это может на самом деле сделать что-то.Это не имеет значения;теперь вы находитесь вне области поведения, определенной в спецификации.Если арифметика указателя не работает, то может показаться, что все работает отлично.

Не то, чтобы компилятор "знал", что в одном случае он не определен, а в другом - определен.Дело в том, что в спецификации не сказано , что произойдет .Может показаться, что работает.Может и нет.Единственный раз, когда он будет работать , это когда он выполняется должным образом в соответствии со спецификацией.

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

0 голосов
/ 16 декабря 2011

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

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

...