6

Is accessing a non-const object through a const declaration allowed by the C standard? E.g. is the following code guaranteed to compile and output 23 and 42 on a standard-conforming platform?

translation unit A:

int a = 23;
void foo(void) { a = 42; }    

translation unit B:

#include <stdio.h>

extern volatile const int a;
void foo(void);

int main(void) {
    printf("%i\n", a);
    foo();
    printf("%i\n", a);
    return 0;
}

In the ISO/IEC 9899:1999, I just found (6.7.3, paragraph 5):

If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

But in the case above, the object is not defined as const (but just declared).

UPDATE

I finally found it in ISO/IEC 9899:1999.

6.2.7, 2

All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.

6.7.3, 9

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type; [...]

So, it is undefined behaviour.

undur_gongor
  • 15,657
  • 5
  • 63
  • 75

5 Answers5

5

TU A contains the (only) definition of a. So a really is a non-const object, and it can be accessed as such from a function in A with no problems.

I'm pretty sure that TU B invokes undefined behavior, since its declaration of a doesn't agree with the definition. Best quote I've found so far to support that this is UB is 6.7.5/2:

Each declarator declares one identifier, and asserts that when an operand of the same form as the declarator appears in an expression, it designates a function or object with the scope, storage duration, and type indicated by the declaration specifiers.

[Edit: the questioner has since found the proper reference in the standard, see the question.]

Here, the declaration in B asserts that a has type volatile const int. In fact the object does not have (qualified) type volatile const int, it has (qualified) type int. Violation of semantics is UB.

In practice what will happen is that TU A will be compiled as if a is non-const. TU B will be compiled as if a were a volatile const int, which means it won't cache the value of a at all. Thus, I'd expect it to work provided the linker doesn't notice and object to the mismatched types, because I don't immediately see how TU B could possibly emit code that goes wrong. However, my lack of imagination is not the same as guaranteed behavior.

AFAIK, there's nothing in the standard to say that volatile objects at file scope can't be stored in a completely different memory bank from other objects, that provides different instructions to read them. The implementation would still have to be capable of reading a normal object through, say, a volatile pointer, so suppose for example that the "normal" load instruction works on "special" objects, and it uses that when reading through a pointer to a volatile-qualified type. But if (as an optimization) the implementation emitted the special instruction for special objects, and the special instruction didn't work on normal objects, then boom. And I think that's the programmer's fault, although I confess I only invented this implementation 2 minutes ago so I can't be entirely confident that it conforms.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • +1 for the quote (although it is a rather fuzzy statement). Regarding your last paragraph: This is what I had in mind when the question came up. I saw a platform where access to `const`s uses a different addressing scheme than access to non-`const`s and was wondering if this is legitimate. – undur_gongor Nov 08 '11 at 17:13
  • I think it is legitimate, bearing in mind (as I said in my answer) that it has to be possible to access non-const objects via a pointer-to-const. So the implementation must be distinguishing between access via a C pointer, and access using the variable name. – Steve Jessop Nov 08 '11 at 17:32
  • I don't think a declaration can provoke UB, only acces to the object could. But 6.7.3/2 states *"The properties associated with qualified types are meaningful only for expressions that are lvalues."* Since here `a` is only present in a declaration and as an rvalue expression this should be allowed. – Jens Gustedt Nov 08 '11 at 17:43
  • @Jens: in C++ it's easy, the expression `a` is an lvalue, to which lvalue-to-rvalue conversion is applied. I can't remember what the equivalent is in C. Why say that the declaration alone can't provoke UB, if it violates a semantic restriction? OK, in practice we know that implementations don't emit any code for a declaration, and that without code a program won't "go wrong" at runtime, but surely that's an implementation detail? For a non-runtime instance of not working: if the linker spots the discrepancy and refuses to link, are you saying that the implementation is non-conforming? – Steve Jessop Nov 09 '11 at 10:07
3

In the B translation unit, const would only prohibit modifying the a variable within the B translation unit itself.

Modifications of that value from outside (other translation units) will reflect on the value you see in B.

This is more of a linker issue than a language issue. The linker is free to frown upon the differing qualifications of the a symbol (if there is such information in the object files) when merging the compiled translation units.

Note, however, that if it's the other way around (const int a = 23 in A and extern int a in B), you would likely encounter a memory access violation in case of attempting to modify a from B, since a could be placed in a read-only area of the process, usually mapped directly from the .rodata section of the executable.

Blagovest Buyukliev
  • 42,498
  • 14
  • 94
  • 130
  • Thank you. This is what I suspected. But still I'd prefer having a justification from the standard or a similar source .. in particular since other developers seem to disagree ;-) – undur_gongor Nov 08 '11 at 14:48
  • @undur_gongor: what you are asking about is really a linker issue, not a language-related one. Such a conflict can be found only by the linker, after the two translation units are compiled. – Blagovest Buyukliev Nov 08 '11 at 15:03
  • Isn't the linking covered by the C standard too? – undur_gongor Nov 08 '11 at 16:46
  • @undur_gongor: I don't think so. Linking is implementation-specific. The standard only talks about external and internal linkage. – Blagovest Buyukliev Nov 08 '11 at 17:05
  • 2
    The details of how linking happens are pretty much up to the implementation. But of course the linker is part of the implementation, so whatever it does must conform to the required behavior of the program (if any -- in this case I don't think there is any required behavior). – Steve Jessop Nov 08 '11 at 17:07
1

The declaration that has the initialization is the definition, so your object is indeed not a const qualified object and foo has all the rights to modify it.

In B your are providing access to that object that has the additional const qualification. Since the types (the const qualified version and the non-qualified version) have the same object representation, read access through that identifier is valid.

Your second printf, though, has a problem. Since you didn't qualify your B version of a as volatile you are not guaranteed to see the modification of a. The compiler is allowed to optimize and to reuse the previous value that he might have kept in a register.

Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
  • The last paragraph is incorrect IMHO. A function call is a sequence point, and if a global variable can be 'cached' over a function call, many programs would break. So, volatile is not necessary or useful. – Johan Bezem Nov 08 '11 at 16:04
  • @JohanBezem: But this is not a regular variable but a `const`. I think the compiler is allowed to assume it is constant. – undur_gongor Nov 08 '11 at 16:10
  • 1
    The variable is extern, and _in this context_ constant. That doesn't mean it is constant everywhere, so IMHO the compiler is not allowed to optimize across the call to `foo()`. – Johan Bezem Nov 08 '11 at 16:23
  • 1
    @JohanBezem, not sure, but being a sequence point is probably not enough as an argument. Then each full statement would oblige the compiler to reload all variables. A function call imposes more than that, since the compiler may not deduce anything about the state of external objects after such a call. On the other hand the declaration is *pretending* that the object is not modifiable, so one could argue that we can assume that it is never modified. This would perhaps be different if we just had a pointer to a `const`, but here the declaration states that the object itself is so. – Jens Gustedt Nov 08 '11 at 16:24
  • "the declaration is pretending that the object is not modifiable, so one could argue that we can assume that it is never modified" - I don't think that follows at all, since the object is `volatile const`, which means it must be read when the abstract machine reads it. An example of a suitable `volatile const` object is a memory location containing the current CPU tick count, or which is mapped to input hardware, so the compiler should not cache the value. An implementation for hardware that has such things, and the facility for the loader to put objects on top of them, can't cache. – Steve Jessop Nov 08 '11 at 16:43
  • Oops, sorry, didn't realise that the `volatile` qualification was added later. My comments apply to the question as it is now, but not as it was before. – Steve Jessop Nov 08 '11 at 16:46
  • I updated the question and added `volatile` (regardless if it is needed or not) because I am not interested in that aspect at the moment. – undur_gongor Nov 08 '11 at 16:50
  • @Steve, out of interest, what would you say for the version without `volatile`? – Jens Gustedt Nov 08 '11 at 17:02
  • 1
    @Jens: without volatile I think you're right, the code in TU B can read `a` once and never again. – Steve Jessop Nov 08 '11 at 17:05
  • Oh, or for that matter it could read `a` never, just have the dynamic linker supply the value as an integer fixup. – Steve Jessop Nov 08 '11 at 17:35
0

Declaring it as const means that the instance is defined as const. You cannot access it from a not-const. Most compilers will not allow it, and the standard says it's not allowed either.

Polynomial
  • 27,674
  • 12
  • 80
  • 107
0

FWIW: In H&S5 is written (Section 4.4.3 Type Qualifiers, page 89): "When used in a context that requires a value rather than a designator, the qualifiers are eliminated from the type." So the const only has an effect when someone tries to write something into the variable. In this case, the printf's use a as an rvalue, and the added volatile (unnecessary IMHO) makes the program read the variable anew, so I would say, the program is required to produce the output the OP saw initially, on all platforms/compilers. I'll look at the Standard, and add it if/when I find anything new.

EDIT: I couldn't find any definite solution to this question in the Standard (I used the latest draft for C1X), since all references to linker behavior concentrate on names being identical. Type qualifiers on external declarations do not seem to be covered. Maybe we should forward this question to the C Standard Committee.

Johan Bezem
  • 2,582
  • 1
  • 20
  • 47
  • 1
    I *think* this means that the qualifiers are eliminated from the type of the value (and hence from the type of the sub-expression that yields that value), not that they're eliminated from the type of any object from which that value may have been loaded. So I *think* it's still a problem that the object designated by `a` in TU A has different type from the object purportedly designated by `a` in TU B. But I admit I haven't found a perfectly clear bit of standardese to support my answer. – Steve Jessop Nov 08 '11 at 17:41