views:

141

answers:

5

C++ standard says that modifying an object originally declared const is undefined behavior. But then how do constructors and destructors operate?

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

I mean here the constructor and destructor do exactly the same thing as the calling code, but they are allowed to change the object and the caller is not allowed - he runs into undefined behavior.

How is it supposed to work under an implementation and according to the standard?

+12  A: 

The standard explicitly allows constructors and destructors to deal with const objects. from 12.1/4 "Constructors":

A constructor can be invoked for a const, volatile or const volatile object. ... const and volatile semantics (7.1.5.1) are not applied on an object under construction. Such semantics only come into effect once the constructor for the most derived object (1.8) ends.

And 12.4/2 "Destructors":

A destructor can be invoked for a const, volatile or const volatile object. ... const and volatile semantics (7.1.5.1) are not applied on an object under destruction. Such semantics stop being into effect once the destructor for the most derived object (1.8) starts.

As background, Stroustrup says in "Design and Evolution of C++" (13.3.2 Refinement of the Defintion of const):

To ensure that some, but not all, const objects could be placed read-only memory (ROM), I adopted the rule that any object that has a constructor (that is, required runtime initialization) can't be place in ROM, but other const objects can.

...

An object declared const is considered immutable from the completion of the constructor until the start of its destructor. The result of a write to the object between those points is deemed undefined.

When originally designing const, I remember arguing that the ideal const would be an object that is writable until the constructor had run, then becomes read-only by some hardware magic, and finally upon the entry into the destructor becomes writable again. One could imagine a tagged architecture that actually worked this way. Such an implementation would cause a run-time error if someone could write to an object defined const. On the other hand, someone could write to an object not defined const that had been passed as a const reference or pointer. In both cases, the user would have to cast away const first. The implication of this view is that casting away const for an object that was originally defined const and then writing to it is at best undefined, whereas doing the same to an object that wasn't originally defined const is legal and well defined.

Note that with this refinement of the rules, the meaning of const doesn't depend on whether a type has a constructor or not; in principle, they all do. Any object declared const now may be placed in ROM, be placed in code segments, be protected by access control, etc., to ensure that it doesn't mutate after receiving its initial value. Such protection is not required, however, because current systems cannot in general protect every const from every form of corruption.

Michael Burr
What about `mutable`?
@STingRaySC: What about `mutable`? `mutable` components of `const` objects can be modified - so those parts aren't `const`.
Michael Burr
+1 ! i find it even more important that it says "const and volatile semantics (7.1.5.1) are not applied on an object under construction/destruction" in the same paragraph.
Johannes Schaub - litb
@litb: I added those more important bits.
Michael Burr
@Michael Burr: I'm going to posit that for most, if not all, implementations `const` objects are not allocated in ROM, particularly not the non-`mutable` parts of them only. Why would an implementation suffer the overhead of "locking" individual bytes of memory (to deal wiht the `mutable` case)? This may not be the case for intrinsic types. The key phrase is "const *semantics* ". It is a logical construct that is enforced by the compiler to *help* guarantee program correctness, nothing more.
@STingRaySC: You're right that an implementation doesn't have to put a `const` object into read-only memory (or memory that can be switched to be read-only). I didn't think that that's what I was saying; however, Stroustrup did envision that this might be possible and allowed for it to happen if an implementation could pull it off. The more important part of the answer (even if it's not the longest part) is that the standard allows ctors and dtors to act on `const` objects. The behavior is such that the object doesn't go '`const`' until the ctor is done and stops being `'const`' for the dtor.
Michael Burr
That seems pretty intuitive to me; in the same sense that an `int` doesn't go `const` until it is initialized. The destructor case is special of course, but there should rarely if ever be a need to modify the object's memory during destruction, no? (discounting amateurish destructors that think they need to zero-out the members or some such thing). But it certainly makes sense that the `const`ness of an object would end at the start of the destructor. The object is not useable after that point and the memory is about to be freed.
@STingRaySC: I don't consider those amateurish. I consider zeroing out pointers in a destructor to be good defensive programming. That practice can catch the use of an object through a stray pointer after destruction in a relatively safe and obvious manner through a segfault as opposed to waiting for memory to be corrupted.
Omnifarious
@Omnifarious: well, you're going to get the seg fault whether you zero out the memory or not! You're going to have to try explaining that again...
@STingRaySC: When you access the dead object's memory and follow one of its pointers, you will get a segmentation fault if that pointer was zeroed out when the object was destroyed.
Omnifarious
@Omnifarious: you will likely get a seg fault accessing the *first* dead object's memory in the first place. Secondly, if you are religiously zero-ing out pointers in destructors, it is not even possible to access that *first* dead object's memory, unless you have a horrible design in the first place!
A: 

Constness for a user-defined type is different than constness for a built-in type. Constness when used with user-defined types is said to be "logical constness." The compiler enforces that only member functions declared "const" can be called on a const object (or pointer, or reference). The compiler cannot allocate the object in some read-only memory area, because non-const member functions must be able to modify the object's state (and even const member functions must be able to when a member variable is declared mutable).

For built-in types, I believe the compiler is allowed to allocate the object in read-only memory (if the platform supports such a thing). Thus casting away the const and modifying the variable could result in a run-time memory protection fault.

+1  A: 

The standard doesn't really say a lot about how the implementation makes it work, but the basic idea is pretty simple: the const applies to the object, not (necessarily) to the memory in which the object is stored. Since the ctor is part of what creates the object, it's not really an object until (sometime shortly after) the ctor returns. Likewise, since the dtor takes part in destroying the object, it's no longer really operating on a complete object either.

Jerry Coffin
A: 

Here's a way that ignoring the standard could lead to incorrect behavior. Consider a situation like this:

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();
}

If optimized, the compiler knows that v is const so could replace the call to v.get() with 5.

But let's say in a different translation unit, you've defined cheat() like this:

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

So while on most platforms this will run, the behavior could change depending on what the optimizer does.

R Samuel Klatchko
+2  A: 

To elaborate on what Jerry Coffin said: the standard makes accessing a const object undefined, only if that access occurs during the object's lifetime.

7.1.5.1/4:

Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.

The object's lifetime begins only after the constructor has finished.

3.8/1:

The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if T is a class type with a non-trivial constructor (12.1), the constructor call has completed.
avakar