Если изменение const-объекта является неопределенным поведением, тогда как конструкторы и деструкторы работают с правами записи? - PullRequest
9 голосов
/ 16 февраля 2010

Стандарт C ++ говорит, что изменение объекта, первоначально объявленного const, является неопределенным поведением. Но тогда как работают конструкторы и деструкторы?

class Class {
public:
    Class() { Change(); }
    ~Class() { Change(); }
    void Change() { data = 0; }
private:
    int data;
};

//later:
const Class object;
//object.Change(); - won't compile
const_cast<Class&>( object ).Change();// compiles, but it's undefined behavior

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

Как это должно работать при реализации и в соответствии со стандартом?

Ответы [ 5 ]

15 голосов
/ 16 февраля 2010

Стандарт явно позволяет конструкторам и деструкторам иметь дело с const объектами. из 12.1 / 4 "Конструкторы":

Конструктор может быть вызван для объекта const, volatile или const volatile. Семантика ... const и volatile (7.1.5.1) не применяется к строящемуся объекту. Такая семантика вступает в силу только после завершения конструктора для наиболее производного объекта (1.8).

А, 12,4 / 2 "Деструкторы":

Деструктор может быть вызван для объекта const, volatile или const volatile. Семантика ... const и volatile (7.1.5.1) не применяется к разрушаемому объекту. Такая семантика перестает действовать, как только начинается деструктор для самого производного объекта (1.8).

В качестве фона, Страуструп говорит в «Дизайн и развитие C ++» (13.3.2 Уточнение определения const):

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

...

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

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

Обратите внимание, что при таком уточнении правил значение const не зависит от того, имеет ли тип конструктор или нет; в принципе все они делают. Любой объект, объявленный const, теперь может быть помещен в ПЗУ, помещен в сегменты кода, защищен контролем доступа и т. Д., Чтобы гарантировать, что он не мутирует после получения своего начального значения. Однако такая защита не требуется, потому что современные системы не могут в целом защитить каждую const от любой формы коррупции.

2 голосов
/ 16 февраля 2010

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

7.1.5.1 / 4:

За исключением того, что любой член класса, объявленный mutable (7.1.1), может быть изменен, любая попытка изменить объект const в течение его времени жизни (3.8) приводит к неопределенному поведению.

Время жизни объекта начинается только после завершения работы конструктора.

3,8 / 1:

Время жизни объекта типа T начинается, когда:

  • получено хранилище с правильным выравниванием и размером для типа T, а
  • если T является типом класса с нетривиальным конструктором (12.1), вызов конструктора завершен.
1 голос
/ 16 февраля 2010

Вот способ, которым игнорирование стандарта может привести к некорректному поведению. Рассмотрим ситуацию, подобную этой:

class Value
{
    int value;

public: 
    value(int initial_value = 0)
        : value(initial_value)
    {
    }

    void set(int new_value)
    {
        value = new_value;
    }

    int get() const
    {
        return value;
    }
}

void cheat(const Value &v);

int doit()
{
    const Value v(5);

    cheat(v);

    return v.get();
}

При оптимизации компилятор знает, что v является постоянным, поэтому может заменить вызов v.get() на 5.

Но, скажем, в другой единице перевода вы определили cheat() следующим образом:

void cheat(const Value &cv)
{
     Value &v = const_cast<Value &>(cv);
     v.set(v.get() + 2);
}

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

1 голос
/ 16 февраля 2010

Стандарт на самом деле мало говорит о том, как реализация заставляет его работать, но основная идея довольно проста: const относится к объекту , а не (обязательно) к памяти в котором хранится объект. Поскольку ctor является частью того, что создает объект, он на самом деле не является объектом до тех пор, пока (некоторое время спустя) не возвращается ctor. Точно так же, поскольку dtor участвует в уничтожении объекта, он больше не работает с целым объектом.

0 голосов
/ 16 февраля 2010

Constness для определенного пользователем типа отличается от constness для встроенного типа. Constness при использовании с пользовательскими типами называется «логическим константностью». Компилятор обеспечивает, чтобы только объекты-члены, объявленные как «const», могли быть вызваны для объекта const (или указателя, или ссылки). Компилятор не может выделить объект в некоторой области памяти, доступной только для чтения, поскольку неконстантные функции-члены должны иметь возможность изменять состояние объекта (и даже const функции-члены должны иметь возможность когда переменная-член объявляется mutable).

Я полагаю, что для встроенных типов компилятору разрешено размещать объект в постоянной памяти (если платформа поддерживает такую ​​вещь). Таким образом, отбрасывание const и изменение переменной может привести к ошибке защиты памяти во время выполнения.

...