Hacker News new | past | comments | ask | show | jobs | submit login
Const and Optimization in C (nullprogram.com)
184 points by adamnemecek on July 25, 2016 | hide | past | favorite | 39 comments



The C99 specification, in §6.7.3¶5, has one sentence just for this:

  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.
This is a separate question than whether the compiler can rely on the function prototype, but doesn't his definition of bar() invoke undefined behavior by this rule?

  void foo(const int *readonly_x) {
    int *x = (int *)readonly_x;  // cast away const
    (*x)++;
  }
I naively assumed that in the context of the bar() function, readonly_x is "an object defined with a const-qualified type", and that since the function modifies this "through use of an lvalue with non-const-qualified type", that bar() invokes undefined behavior. Or am I falsely equating "declaring an object" with "defining an object"?

The original x wasn’t const-qualified, so this rule didn’t apply. And there aren’t any rules against casting away const to modify an object that isn’t itself const.

Does the original x matter here, or just the readonly_x variable that is in scope of the function? In the language of the spec, are x and readonly_x the same object? Or two different objects of different types that happen to point to the same address? Is it certain that const-qualifiers on function arguments can be legally be ignored inside that function?


`readonly_x` is a non-constant object of type 'pointer to const int'. If `readonly_x` itself was const-qualified, it'd look like this: `const int* const readonly_x`.

This is a distinct object from whatever pointer the caller is passing in. But none of this matters to the special const rule -- that rule is not talking about the pointer object, but about the target object that the pointer is pointing to.


Thanks, that goes a long way toward an explanation. I'm still a bit surprised though that no compilers seem to complain even if we cast away the const from the properly const-qualified version. Here's the snippet I was playing with that tests all the variations: https://godbolt.org/g/aaC4B7

I should have expected it, but if you define the "lying" function where the compiler can see the full definition (and don't specify -fno-inline), the loads are optimized out. It made me wonder though whether some variation of this bug might apply : http://www.playingwithpointers.com/ipo-and-derefinement.html


[Saw a response here, but it disappeared before my reply. Posted here anyway in case it clarifies my first paragraph.]

Did you check out the link? https://godbolt.org/g/aaC4B7

My surprise is that none of clang, gcc, or icc give any warning on this with -Wall -Wextra:

  1  void copy_const(const int * const arg) {
  2    int *copy = (int *)arg;
  3    (*copy)++;
  4  }
I agree that the cast on line 2 is legal and requires no warning. My surprise is that there is no warning for the write on line 3, since I think this is undefined behavior according the quoted part of the spec. Isn't this trying "to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type"?

I realize that the compiler has no obligation to issue a warning here, and in fact is fully entitled to my first-born as soon as it encounters undefined behavior. Still, it seems like it would be a useful place to offer the user a warning just as it does in the case of direct modification:

  1 void write_const(const int * const arg) {
  2   (*arg)++;
  3 }
clang: "error: read-only variable is not assignable"

gcc: "error: increment of read-only location '*arg'"

icc: "error #137: expression must be a modifiable lvalue"


In gcc try -Wcast-qual


It's good to know that it exists (and that it's not included in -Wall -Wextra) but it's not quite what I'm looking for. The cast itself is well defined --- it's the subsequent write that is the issue. While it wouldn't be possible to catch all of these, those that are caught are likely genuine bugs.

That said, it looks like -Wcast-qual is also supported by clang and icc. Clang includes it in "-Weverything", which gcc and icc do not support. Even if not ideal for this issue, I'm sure there are cases where it would help to catch bugs.


One of the subtler distinctions between C and D is D does allow this optimization. This occasionally engenders debate about which is better.


What is the argument in favor of specifically allowing casting away const of pointers?


The strongest argument is to support a sort of poor man's const-generics. Consider a function like strchr(). This locates a character in a (const) string, and returns a (non-const) pointer to it.

The idea is that you can use this on both const and non-const strings. Call it on a const string, you get back a non-const pointer (which you better treat as const!) But call it on a non-const string, you get back a non-const pointer, with which you can mutate the string. So a single function serves both const and non-const uses.

If casting away const-ness were disallowed, the compiler might conclude that strchr()'s returned value cannot alias the input string, and its optimizations would defeat this design. Anyways that's the original rationale.


It does not strike me as much of a benefit, and certainly not worth the cost of creating confusing semantics (in fact, I would prefer (a true) const to be the default in any language, with something like 'mutable' required to override.)


And that's why D has the 'inout' qualifier


- In some case implementing both "const" and "non const" methods cause a lot of code duplications and cast can help.

- Compatibility with old libraries


> implementing both "const" and "non const" methods cause a lot of code duplications

D has an `inout` qualifier that has the effect of "transmitting" the const-ness of an argument to the return type.


Logical const and expressiveness.


So if I understand the author, const function arguments can just have const-ness cast away inside the function, so it's really much closer to a type hint than anything else. However, if a variable is declared const, casting away const-ness is undefined. Is this about right? Yikes that's complex. I suppose the moral is casting away const-ness is a terrible idea.


The key distinction is not variable vs function argument, but rather const variable vs pointer-to-const.

If you have "const int x" (as a local variable, global variable, or function parameter) then there is no valid way to modify x's value. But if you have "const int* y" (as a local variable, global variable, or function parameter), you can always cast away const and mutate as long as the original variable (the one "y" points to) wasn't declared const.


"casting away const-ness is undefined"

No, it's not. That's the whole point why compiler can't optimize it away. You can often see ("char *") casts from static "strings" in legacy APIs and libraries calls, because original authors didn't know or didn't care how to use const correctly (or at all). I, personally, use const a lot throughout my C code, when you get it, it makes debugging so much easier.


you skipped the first clause of my sentence --

if a variable is declared const, casting away const-ness is undefined

is that not true?


To reinforce the sibling comment's point: A very common pattern is when a const variable resides in non-writable memory (happens all the time in embedded), - if you attempt to write to it, you trigger a fault.

So if you cast away constness to conform to some API and then read the value, everything is fine (aside from questionable API design, of course). Modifying the value is another story, though.


Your first part was irrelevant: if you cast away const, of course variable must have been const in first place. As other have already mentioned, casting itself is not an undefined behavior (and sometimes even have legitimate reasons, mentioned in my earlier post). On the other hand, trying to modify const variable though non-const pointer is undefined behavior.

P.S. if you downvoted to disagree with this (on my parent post), please provide an opposite example, because as a C programmer, I would really be interested to be proven wrong on this matter.


No, it could be mutable variable referenced/accessed (not sure of the right word here) in a function via either a reference-to-const or a pointer-to-const.

It seems highly non-intuitive to me that casting away constness works differently depending on whether the variable was defined const or not, but if that's the rules, that's the rules.


The cast itself is not undefined.

It's completely valid to use a non-const pointer to a const variable for reading that variable (this is actually quite common when interacting with libraries that are not const-correct). Undefined behavior only occurs when the const variable is being modified.


I think it is only modifying that non-const value that is undefined. If you use a nominally mutable value in a read-only way, then you are OK.

This is important because older libs that ignore (or abuse) const will often do read-only access to a char* or something. A nice example is that POSIX defines

    int execv(const char *path, char *const argv[]);
That definition takes a constant pointer to variable chars! Worse, some people use the same signature for `main()` and also for libraries that parse `argv`. But none of them are actually allowed to vary those chars under Unix.


Yes, if a variable is declared const, casting away const-ness from a pointer to it and then writing to it is undefined.


Mutability of a variable and const qualification of the target type of the pointer used to store its address are independent, and language semantics really only care about the former.


It's just that the actual const-ness is a property of the object itself, not something imbued by the lvalue used to access the object.


The following is a foo function that take a constant pointer:

    void foo(int *const x);
If you point x to another adress inside foo function, it will not compiled.

The author seems think that

    void foo(cont int *x);

is function that takes a constant pointer which is wrong, it is a function that takes pointer to constant object. In this case, it is legal if you point x to another address in memory inside foo function.


Where do you see this confusion? I don't think it's there.

The author points out that it is not necessarily undefined behavior to take your second prototype, cast the const away and modify the pointed-to object if you ensure that it is only called with non-constant objects.


In 2nd paragraph, the author states:

    void foo(const int *);
    ...
> The function foo takes a const pointer, which is a promise from the author of foo that it won’t modify the value of x. Given this information, it would seem the compiler may assume x is always zero, and therefore y is always zero.


It is clear that the author means "pointer to const" there.


The author does not think that, all he does is point out correctly that const-qualifying a pointer's target type cannot be used for optimization purposes as it does not make any actual guarantees about object mutability.

Note that in contrast, restrict-qualifying a ponter-to-const (ie `const int *restrict x`) does make such guarantees, but only callee-side.


Update: The author have been corrected the 2nd paragraph. My comment above is not valid anymore


I've personally stopped using const in my C and C++ code, except for actually defining constants (in the #define sense). The headaches and complexity it introduces don't seem worth the benefit - you inevitably have to remove the const modifiers from your "const correct" function at some point when it needs to calls out to a non-const function you don't control (one that you know is 'logically const' and pure but doesn't declare it as such in the signature). And as this article demonstrates, the benefits to optimization are not always that great.

And with any kind of STL-like interface or container you're suddenly maintaining two duplicate versions of everything, a non-const version and a const version, likely along with a confusing pile of template metaprogramming and typedefs to support that. Avoiding const altogether is much cleaner.

As Casey Muratori (game programmer) once said, "I haven't typed "const" in over a decade, and I have had literally zero bugs that it would have caught."


I feel like this is analogous to throwing the baby out with the bath water.

I think writing const-correct code where you can is useful. It helps document your code (for yourself and for others), it CAN catch issues (even if Casey Muratori claims it never helped), and it really isn't that hard to do (C routines don't need to be duplicated if you want to return non-const, like strstr, for example).

So what if you have to cast-away-const for interfacing to libraries which aren't const-correct? It still has benefits for your own code.


Here is the gist of the explanation from the article: And there aren’t any rules against casting away const to modify an object that isn’t itself const. This means the above (mis)behavior of foo isn’t undefined behavior for this call. Notice how the undefined-ness of foo depends on how it was called.

Wtf? This seems like a huge potential optimization gain missed. Surely by know it can't just be enabled because a lot of code would be written with the assumption above, I'm just curious for the rationale of this model. I don't understand the reasoning at all, why would you ever want to cast from a const to non const and have the behaviour depend on the type I pass to the function? Let's assume the signature of foo was faulty definied as const but still modifies x inside (faulty definined signatures for API compatibility seems to be the whole reason for allowing you to cast away const) and I pass int x, the the behaviour is definied but if I pass a const int x the behaviour is undefined?!? Clearly this is the fault of the person writing the foo function but how does that help me writing the bar function, I am not expected to know the whole call stack of foo, that's what the signature is for, I must be able to trust it's const correctness and that should not depend on what I pass into it.

If the optimization was always disabled I could understand the reasoning but now you get some kind of half assed middle ground where you as a caller must guarantee the actions of a callee.


Is there any portable way to tell the compiler it can do this kind of optimization in C ? Wouldn't that be useful ?


"void foo(const int * );"

"The function foo takes a const pointer".

I thought foo was taking a pointer a constant, where the pointer itself could change. This may seem pedantic, but perhaps the optimization he expected would have worked if foo took "const int * const ".


`const int * ` and `int const * ` are semantically identical, ie `const int const * ` is a duplicate declaration.


Opps, I'll update my post. Looks like peeyek had already pointed out that the pointer isn't const anyway.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: