Accredited Standards Committee X3 Doc No: X3J16/94-0184 WG21/N0571 Information Processing Systems Date: Sept 27, 1994 Page 1 of 16 Operating under the procedures of Project: Programming Language C++ American National Standards Institute Ref Doc: Reply to: Josee Lajoie (josee@vnet.ibm.com) +---------------+ | const Objects | +---------------+ 1. What are "const objects"? ============================ In the WP, the term "const object" has 2 meanings: M1. relates to the type-system When the working paper says "const object", it sometimes intends to say that the static type of a variable or expression nominating an object is const. M2. relates to the storage When the working paper says "const object", it sometimes intends to say that the memory where the object is located may be write-protected. That is, a variable or expression of const type may denote an object stored in write-protected memory (implementation-defined) and any attempt to modify the object results in undefined behavior. Reading the core email discussions on the topic of const objects, everybody seemed to agree that the term "const object" in the WP can be interpreted to mean either M1 or M2 above. Most expressed the desire to make the wording of the WP clearer by using a different terminology for these 2 meanings. I address this further in Appendix A and B below. Open Issue: ----------- There is one remaining open issue however. We need to decide if the 'const' attribute also influences the runtime properties of an object? My opinion is that it doesn't. I believe this is the behavior currently supported by C++. I defend this position below and propose some WP changes to make this clear. 1.1 alignment and size ---------------------- Section 9.2[class.mem](!) says: "The qualified or unqualified versions of a type are distinct types that have the same representation and alignment requirements." This clearly indicates that cv-qualifiers do not influence the shape (see document 94-0182) of an object. -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 2 Proposal: - - - - - Move the sentence listed above sub-clause 3.7.2 [basic.compound]. 1.2 construction/destruction ---------------------------- Constructors and destructors cannot be cv-qualified. 12.1 [class.ctor] "A constructor can be invoked for a const or volatile object. A constructor may not be declared const or volatile." 12.4 [class.dtor] "A destructor can be invoked for a const or volatile object. A destructor may not be declared const or volatile." This indicates that non-qualified constructors and destructors are used for the objects of a particular class type, whether their declaration specifies a cv-qualifier or not. I think this is deliberate. I believe it follows from point 1 above indicating no difference between the shape of objects with cv-qualified declarations and those without. 1.3 calls to member functions ----------------------------- The question related to this topic is the following: struct A { void f(); }; A a1; // 'a1' nominates a non-const object const A a2; // 'a2' nominates a const object const A* pa1 = &a1; // 'pa1' is a const access path to a // non-const object const A* pa2 = &a2; // 'pa2' is a const access path to a // const object const_cast(pa1)->f(); //1 OK. const_cast(pa2)->f(); //2 ? The question is, does the call on line //2 have undefined behavior? That is, is const an attribute of the dynamic type of the object? If f does not modify the object a2, is the call itself undefined because of the "dynamic type" mismatch between the type of the object and the type of the member function? Though the wording in 9.4.1 [class.this] is not completely clear, I believed everybody agrees to say that, no, the call itself is not undefined. Only if the member function modifies the object (i.e. attempts to write to storage that may be write-protected) will the call have undefined behavior. For example: -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 3 struct A { int i; void f(); }; const A a2; const A* pa2 = &a2; const_cast(pa2)->f(); //1 void A::f() { ++i; //2 }; Because 'f' modifies its object (line //2), the call on line //1 may result in undefined behavior. I believe this behavior is covered by the WP already: 5.2.10 [expr.const.cast] "Depending on the type of the object, a write operation through the pointer, lvalue or pointer to data member resulting in a const_cast that cast-away constness may produce undefined behavior." However, section 9.4.1 could use a little bit of editorial clean up regarding the description of cv-qualifiers. proposal: - - - - - I believe sub-clause 9.4.1 [class.this] paragraph 2, 3 and 4 should be replaced with the following: | 2 A cv-qualified member function (that is, a member function | declared with the const and/or volatile qualifiers) may be | called for an object-expression (_expr.ref_) only if the type of | the object-expression is as qualified or less-qualified than the | member function. For example: [ In an earlier paragraph: struct s { int a; int f() const; int g() { return a++; } int h() const { return a++; } // error }; ] int s::f() const { return a; } void k(s& x, const s& y) { x.f(); x.g(); y.f(); y.g(); // error } -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 4 | The call y.g() is ill-formed because the type of the | object-expression y is const and s::g() is a non-const member | function; that is, is less qualified than the type of the | object-expression. [ Note: I believe the undefined behavior case discussed earlier, in which a const_cast is used to hide the constness of the object-expression, is covered by the section on const_casts and doesn't need to be discussed here. However, if anyone has strong feelings about this, a sentence could be added here to discuss it. i.e. "If a const_cast is used to hide the constness of the object-expression and a non-const member function is called that modifies the object for which it was called, the program has undefined behavior." ] | 3 Constructors (12.1) and destructors (12.4) cannot be declared | const or volatile however these functions may be invoked to | create objects declared with cv-qualifiers. 1.4 typeid ---------- Does typeid indicate cv-qualifiers? After reading the core email discussions, I propose that the typeid operator ignore the top-level cv-qualifiers of its operand's type. That is, whether the operand of the typeid operator is an expression or a type-name, the top-level cv-qualifiers will be ignored. [Bill Gibbons in message c++std-core-4212:] const D d2; typeid(d1) == typeid(d2); // ?? typeid(D) == typeid(const D); // ?? and, worse yet, typedef D T1; typedef const D T2; if (typeid(T1) == typeid(T2)) assert(typeid(T1*) == typeid(T2*)); The typeinfo's of T1* and T2* cannot be the same; the constness is an important part of the type. Yet if the typeinfo's of T1 and T2 are the same, it would be very strange for the typeinfo's of T1* and T2* to differ. The only way I see to resolve this is to say that top-level qualifiers of the type of an *expression* are ignored. -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 5 Applying these rules to *type* arguments of typeid would result in some pretty strange results. So expression arguments and type arguments have to be inconsistent. This is the least painful inconsistency, since the two kinds of operands are different things already. [John Bruns in message c++std-core-4222:] I would agree that [ the result of: if (typeid(T1) == typeid(T2)) assert(typeid(T1*) == typeid(T2*)); ] is a weird result, but I think it would be the best one possible in the situation we are in. If you consider Jonathan's comment [about access paths], it can make some sense. In fact T1 and T2 are the same type, T2 can only be reached by a restricted access path. Checking *T1 and *T2 we find that the accesses (pointers) do indeed differ. I agree with the resolution [of ignoring the top-level cv-qualifiers of the type of an expression ]. I would prefer to see the top level cv-qualifiers stripped when comparing type-id's. If we ignore cv-qualifiers [on both expression and type arguments], don't we resolve that expression arguments and type arguments behave consistently?? [Richard Minner in message c++std-core-4243:] The main problem I have with typeid() including const is that it won't "work right" with non-polymorphic objects anyway. (The utility of typeid() for non-polymorphic objects is already suspect, but other than constness it at least gives the right answer.) For example: int i; const int* ip = &i; assert(typeid(*ip) == typeid(const int)); This would say that ip points at a "const int" which is wrong. It _does_ point at an int, but the 'const' is part of the expression _only_. For a polymorphic type it could give the right answer (the actual "constness" (readonlyness) of the object itself) but I think the inconsistency would be confusing. [Jerry Schwarz in message c++std-core-4265:] If typeid encodes "const" the situation is even worse than that. Suppose I have -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 6 class Base { } ; class D1 : public Base() { } ; class D2 : public Base() { } ; and I wish to register the derived classes in some kind of lookup table. I would like to be able to write something like: Map map ; void setup(const typeinfo& d, Info i) { map.set(&d, i) ; } .... setup(typeid(D1), ...) ; .... But that would only register the non-const typeinfo. And note that there is no way when the table is setup to determine if the typeinfo represents a const, and there is no way to transform the non-const typeinfo into the const one. I am convinced that typeinfo should not reflect "constness" of the type. proposal: - - - - - Add to section 5.2.7 [expr.typeid] the following sentence: "typeid ignores the top-level cv-qualifiers of its operand's type." Summary: -------- As points 1 to 4 indicate, I believe the cv-qualifers do not influence the runtime properties of an object. I believe this is the current state of the C++ language. The places in the WP that talk about "const objects" will have to be reworded. See appendix A for the proposed rewording. 2. Objects in write-protected storage ===================================== I must first say that the material in this section is greatly inspired from the text Mike Anderson wrote in message c++std-core-4605. Thanks Mike for all this work. The working paper says: 7.1.5.1 [dcl.type.cv]: "any attempt to modify a const object after it has been initialized and before it is destroyed results in undefined behavior." For classes with constructors and destructors, this means that the object can be written to during construction (and destruction) but the -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 7 object may be "write-protected" for some time during the execution of the program, that is, between the time the object initialization has completed and before the object destruction starts (i.e. for the duration of its lifetime). I know of no implementation today that currently supports the dynamic "write protection" mechanism described here; that is, I know of no implementation that write protects the memory in which an object resides if the object is dynamically initialized. However, since the WP already allows for implementations to support such a mechanism for objects with automatic or static storage duration, some have asked that this support be extended for objects allocated on the heap. I see 2 ways of resolving this issue: S1. No, C++ does not allow for write-protected objects to be created on the heap. S2. Yes, C++ allows for write-protected objects to be created on the heap. Simple, isn't it? ;-) I will present below the 2 possibilities and we can choose the one that is most appropriate. 2.1 S1: write-protected objects cannot be created on the heap ------------------------------------------------------------- 2.2.1 type of a new-expression If we decide not to support the creation of write-protected objects on the heap, I believe it will be less surprising for users if the WP simply prohibits cv-qualified types to be used in new-expressions. Proposal: - - - - - In 5.3.4 [expr.new] change the following sentence: "The type-specifier-seq shall not contain const, volatile, class declarations, or enumeration declarations." to: "A cv-qualified type shall not be used in a new expression to specify type of the object to be created. The type-specifier-seq shall not contain class declarations, or enumeration declarations." Example: const int *p1 = new const int; // Error const int *p2 = new (const int); // Error typedef const int CI; const int *p3 = new CI; // Error -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 8 Argument supporting S1: ----------------------- No one has proven the need to provide language support for read-only heap storage. 2.2 S2: write-protected objects can be created on the heap ---------------------------------------------------------- If we decide to support S2, certain areas of the language will need to be clarified. 2.2.1 type of a new-expression The restriction on using const or volatile keywords in type-specifier-seq in new-type-id should be removed. Note that this change also applies to objects created with placement new. If the type specified with placement new is const, the object created may be write-protected. Proposal: - - - - - In 5.3.4 [expr.new] change the following sentence: "The type-specifier-seq shall not contain const, volatile, class declarations, or enumeration declarations." to: "The type-specifier-seq shall not contain class declarations, or enumeration declarations." Example: const int *p1 = new const int; // Ok const int *p2 = new (const int); // Ok typedef const int CI; const int *p3 = new CI; // Ok 2.2.2 storage The WP will need to indicate that objects allocated by a new expression of const type cannot be modified. Proposal: - - - - - In 7.1.5.1 [dcl.type.cv], add the following sentence: "When specified in the type of an object allocated by a new expression, const means the program may not change the object." This means that applying a const_cast to a pointer to a write-protected object and then assigning to that object results in undefined behavior. On implementations with write-protected heap storage, it could be an abort at runtime; on other implementations there would be no noticeable effect. -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 9 Example: const int *p = new const int(0); const int *q = new int; *(const_cast(p)) = 0; // undefined <== change *(const_cast(q)) = 0; // OK 2.2.3 initialization A declared static or automatic object that is declared with a const type is required to have an initializer; for consistency (and for similar reasons), so should objects dynamically created by a new expression of a const type. Proposal: - - - - - In 5.3.4 [expr.new] add the following sentence: "An initializer is required for an object created by a new expression of const type if the type has no user-defined constructor. Failing to obey this rule is an diagnosable error at compile time. Example: const int *p = new const int; // ill-formed <== change const int *q = new const int(0); // OK const int *r = new int; // OK 2.2.4 destructors or deleted Can const objects allocated on the heap be explicitly destroyed or deleted? Is a const_cast required? For example: class T {}; void f ( const T* pcT ) { pcT->~T(); // Legal? const_cast(pcT)->~T(); // or is const_cast required? } 2.2.4.1 necessary const_cast The WP already says (5.3.5 [expr.delete]): "A C++ program that applies delete to a pointer to constant is ill-formed." Following this lead, the WP may require that a const_cast be used to cast away the constness of the pointer to delete a write-protected object. This provides a "reasonable barrier for parameters of type 'pointer to const'" (Erwin, core-4542; see also Bjarne, core-4576). -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 10 Example: const int *p = new const int(0); const int *q = new int; delete p; // error delete const_castp; // Ok delete q; // Ok delete const_castq; // Ok 2.2.4.2 Undefined behavior Or is destroying or deleting an object after applying a const cast an undefined operation? Example: const int *p = new const int(0); const int *q = new int; delete p; // error delete const_castp; // undefined behavior? delete q; // error delete const_castq; // Ok If it is then the WP should be modified to say (5.3.5 [expr.delete]): "A C++ program that applies a destructor or a delete to a pointer an object that was allocated by a new expression of const type results in undefined behavior." It seems a bit strange though to never be able to destroy or delete an object allocated on the heap and this is therefore my least favorite option. 2.2.4.3 unnecessary const_cast Example: const int *p = new const int(0); delete p; // Ok? The arguments in favor of this option have been: o It doesn't seem consistent to have objects created by "new T" have the same representation as objects created by "new const T" and yet require that different delete expressions be used for these objects. (rtm in core-4563 and core-4588). o Circumventing the restriction with a const_cast is common but "ugly". [Richard Minner in message core 4604:] If you have to use a const cast to do a delete, then you will be littering you code with const_casts, and the major visual impact will be that the original intent of const_casts "if you spot a new style cast, the programmer is doing something 'naughty' or unusual worthy of investigation" will be lost. We'll get so used to const casts in delete, we'll just ignore them. Users won't want to HAVE to write that cast in "normal" code because it defeats the purpose of having the casts. -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 11 o Not requiring const_cast solves a problem in writing templates that use the delete operator on a template parameter. Proposal: - - - - - If this solution is adopted, the following sentence in 5.3.5 [expr.delete] must be deleted: "A C++ program that applies delete to a pointer to constant is ill-formed." Argument supporting 2.2: ------------------------ 2.2 makes the memory model and object model more consistent, because the properties of objects created on the heap are similar to those of objects with static and automatic storage duration. Appendix A - Editorial changes ============================== First, I propose to merge sub-clause 3.7.3 [basic.type.qualifiers] with sub-clause 7.1.5.1 [dcl.type.cv] because both sub-clauses describe the same thing: cv-qualifiers. See appendix B for the proposed words of the new sub-clause. Also, the WP uses the term "const objects" and objects in many places when it really ought to talk about the variables or expressions designating these objects. If the resolution proposed in section 1 above is approved (i.e. if const is not a runtime property of an object), then the WP will need to be reworked to remove uses of the term "const objects". Below are the references to the term "const objects" and "objects" I have found in the WP that I believe need rewording. You will also find below the proposed rewording. 5.2.2[expr.call] p3 In addition, it is possible to modify the values of nonconstant objects through pointer parameters. should become: In addition, it is possible to modify the values of objects through non-const pointer parameters. 5.2.10 [expr.const.cast] p8 Depending on the type of the object, a write operation through the pointer, lvalue or pointer to data member resulting in a const_cast that cast-away constness may produce undefined behavior." should become: Depending on the type of the variable or of the new expression creating the object, a write operation through the pointer, lvalue or pointer to data member resulting in a const_cast that cast-away constness may produce undefined behavior." -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 12 5.3.1[expr.unary.op] p2 In particular, the address of an object of type cv T is pointer to cv T, with the same cv-qualifiers. For example, the address of an object of type const int has type pointer to const int. should become: In particular, the type of the expression '&x', where 'x' has type 'cv T', is pointer to 'cv T', with the same cv-qualifiers. For example, if 'x' has the type 'const int', the expression '&x' has type pointer to const int. 5.3.1[expr.unary.op] p3 The address of an object of incomplete type may be taken, but only if the complete type of that object does not have the address-of operator (operator&()) overloaded; no diagnostic is required. should become: If 'x' in the expression '&x' has incomplete type, the expression has well-defined behavior only if the complete type of 'x' does not have the address-of operator (operator&()) overloaded. 5.3.5 [expr.delete] p6 If the class of the object being deleted is incomplete at the point of deletion and the class has a destructor or an allocation function or a deallocation function, the result is undefined. should become: If the operand of operator delete is of incomplete class type at the point of deletion and the class has a destructor or an allocation function or a deallocation function, the result is undefined. 7.1.1 [dcl.stc] p9 The mutable specifier on a class data member nullifies a const specifier applied to the containing class object and permits modification of the mutable class member even though the rest of the object is const (7.1.5.1). should become: The mutable specifier on a class data member nullifies the const qualifier in the type of the variable or expression representing the member's containing class and permits modification of the mutable class member even though its containing class is const (7.1.5.1) and cannot be modified. 8.3.1 [dcl.ptr] p2 ci = 1; // error ci++; // error *pc = 2; // error cp = &ci; // error cpc++; // error p = pc; // error ppc = &p; // error Each is unacceptable because it would either change the value of an object declared const or allow it to be changed through an unqualified pointer later, for example: should become: Each is unacceptable because it would either change the value of an expression of const type or allow a variable declared const to be -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 13 changed through an unqualified pointer later, for example: 8.5 [dcl.init] p3 An expression of type cv1 T can initialize an object of type cv2 T independently of the cv-qualifiers cv1 and cv2. should become: An expression of type cv1 T can initialize a declaration of type cv2 T independently of the cv-qualifiers cv1 and cv2. 8.5 [dcl.init] p7 When an initializer applies to a pointer or an object of enumeration or arithmetic type, it consists of a single expression, perhaps in braces. The initial value of the object is taken from the expression; should become: When an initializer applies to a declaration of enumeration or arithmetic type, it consists of a single expression, perhaps in braces. The initial value of the object denoted by the declaration is taken from the expression; 8.5.3 [ New sub-clause - work in progress ;-) ] 9.4.1 [ new wording suggested in section 1.3 of this paper ] 12.1 [class.ctor] p1 A constructor can be invoked for a const or volatile object. should become: A constructor can be invoked for an object created by the declaration of a const or volatile variable. 12.3.2 [class.conv.fct] p1 A conversion operator is never used to convert a (possibly qualified) object (or reference to an object) to the (possibly qualified) same object type (or a reference to it), or to a (possibly qualified) base class of that type (or a reference to it). should become: A conversion operator is never used to convert an expression of a (possibly qualified) object type to the (possibly qualified) same object type (or a reference to it), or to a (possibly qualified) base class of that type (or a reference to it). 12.4 [class.dtor] p1 A destructor can be invoked for a const or volatile object. should become: A destructor can be invoked for an object created by the declaration of a const or volatile variable. 12.4 [class.dtor] p9 Invocation of destructors is subject to the usual rules for member functions, e.g., an object of the appropriate type is required (except invoking delete on a null pointer has no effect). should become: Invocation of destructors is subject to the usual rules for member functions, e.g., an object-expression of the appropriate type is required (except invoking delete on a null pointer has no effect). -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 14 12.8 [class.copy] p2 Otherwise it will have a single parameter of type X&: X::X(X&) and programs that attempt initialization by copying of const X objects will be ill-formed. should become: Otherwise it will have a single parameter of type X&: X::X(X&) and programs that attempt initialization by copying of object-expressions of type const X will be ill-formed. 12.8 [class.copy] p6 Otherwise it will have a single parameter of type X&: X& X::operator=(X&) and programs that attempt assignment by copying of const X objects will be ill-formed. should become: Otherwise it will have a single parameter of type X&: X& X::operator=(X&) and programs that attempt assignment by copying of object-expressions of type const X will be ill-formed. 13 [ New clause - work in progress ;-) ] 15.1 [except.throw] p1 An object is passed and the type of that object determines which handlers can catch it. should become: An object is passed and the type of the expression denoting the object determines which handlers can catch it. 15.3 [except.handle] p2 A handler with type T, const T, T&, or const T& is a match for a throw-expression with an object of type E if should become: A handler with type T, const T, T&, or const T& is a match for a throw-expression with an operand of type E if Appendix B - merge sub-clause 3.7.3 and 7.1.5.1 =============================================== I propose to merge sub-clause 3.7.3 with sub-clause 7.1.5.1. The new proposed sub-clause 7.1.5.1 appears here: 7.1.5.1 The cv-qualifiers [dcl.type.cv] 1 There are two cv-qualifiers, const and volatile. When present in the declaration of an object or when specified in the type of an object allocated by a new expression, const means the program may not change the object. Except that any class member declared mutable (7.1.1) may be modified, any attempt to modify an object created by a declaration -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 15 of const type after the object has been initialized and before it is destroyed results in undefined behavior. Example class X { public: mutable int i; int j; }; class Y { public: X x; Y(); } const Y y; y.x.i++; // defined behavior y.x.j++; // undefined behavior Y* p = const_cast(&y); // cast away const-ness of y p->x.i = 99; // defined behavior p->x.j = 99; // undefined behavior const Y *q = new const Y; q->x.i = 88; // defined behavior const_cast(q)->x.i = 88; // defined behavior const_cast(q)->x.j = 88; // undefined behavior 2 There are no implementation-independent semantics for objects declared with a volatile qualifier; volatile is a hint to the compiler to avoid aggressive optimization involving the object because the value of the object may be changed by means undetectable by a compiler. Each element of a volatile array is volatile and each nonfunction, nonstatic member of a volatile class object is volatile (9.4.1). +------- BEGIN BOX 39 -------+ Notwithstanding the description above, the semantics of volatile are intended to be the same in C++ as they are in C. However, it's not possible simply to copy the wording from the C standard until we understand the ramifications of sequence points, etc. +------- END BOX 39 -------+ A declarations may have both cv-qualifiers. 3 There is a (partial) ordering on cv-qualifiers, so that one object type or pointer type may be said to be more cv-qualified than another. Table 10 shows the relations that constitute this ordering. Table 10-relations on const and volatile ______________________________________ | no cv-qualifier < const | | no cv-qualifier < volatile | | no cv-qualifier < const volatile| | const < const volatile| | volatile < const volatile| |_____________________________________| -------- X3J16/94-00184 - WG21/N0571 ----- Lajoie:const Objects ---- Page 16 4 A pointer or reference to cv-qualified type (sometimes called a cv- qualified pointer or reference) need not actually point to a cv- qualified object, but it is treated as if it does. For example, a pointer to const int may point to an unqualified int, but a well- formed program may not attempt to change the pointed-to object through that pointer even though it may change the same object through some other access path. CV-qualifiers are supported by the type system so that a cv-qualified object or cv-qualified access path to an object may not be subverted without casting (5.4). For example: void f() { int i = 2; // not cv-qualified const int ci = 3; // cv-qualified (initialized as required) ci = 4; // error: attempt to modify const const int* cip; // pointer to const int cip = &i; // okay: cv-qualified access path to unqualified *cip = 4; // error: attempt to modify through ptr to // const int* ip; ip = cip; // error: attempt to convert const int* } 5 Unless explicitly declared extern, a const object does not have external linkage (3.4) and must be initialized (8.5; 12.1). A varaible of const integral type initialized by a constant expression may be used in constant expressions (5.19). Each element of a const array is const and each non-function, non-static, non-mutable member of a const class object is const (9.4.1).