1

I am designing an API and am considering the use of "immutable" structs", or "read-only structs". Using a simplified example, this could look something like:

struct vector {
    const float x;
    const float y;
};

struct vector getCurrentPosition(void);
struct vector getCurrentVelocity(void);

Everything works fine as long as I return the immutable struct on the stack. However, I run into issues when implementing a function like:

void getCurrentPositionAndVelocity(
    struct vector *position,
    struct vector *velocity);

At this point, I do not want to introduce a new immutable struct that contains two vectors. I also can not do this:

void
getCurrentPositionAndVelocity(
    struct vector *position,
    struct vector *velocity)
{
    *position = getCurrentPosition();
    *velocity = getCurrentVelocity();
}

because position and velocity are read-only (although my installed version of clang incorrectly compiles this without warning).

I could use memcpys to work around this, like this:

void
getCurrentPositionAndVelocity(
    struct vector *position,
    struct vector *velocity)
{
    struct vector p = getCurrentPosition();
    struct vector v = getCurrentVelocity();

    memcpy(position, &p, sizeof *position);
    memcpy(velocity, &v, sizeof *velocity);
}

This looks bad but I believe it does not mislead the user of the API, to whom the vector struct still looks immutable. I could additionally add a required initializer for this purpose, where a call to a function like this would only succeed for vector structs with a special value. Something like:

const struct vector * const VECTOR_UNINITIALIZED;

where the user should do

struct vector position = *VECTOR_UNINITIALIZED;
struct vector velocity = *VECTOR_UNINITIALIZED;

before invoking getCurrentPositionAndVelocity(). Before memcpy-ing, the implementation would assert with a memcmp that the vectors do have the "uninitialized sentinel" value. (Typing this, I realize that this will only work if there are certain truly unused values that can act as "uninitialized sentinel" values but I think that is the case for me.)

My question is whether this usage of the const keyword is in line with its intention, from the API user's perspective? And would there be any risks involved from a compiler perspective, in that this code may, strictly speaking, violate the read-only semantics as indicated with the const keyword? As an API user, what would you think of this approach or would you have any better suggestions?

Reinier Torenbeek
  • 16,669
  • 7
  • 46
  • 69
  • 1
    tl;dr. [mcve]?? – Swordfish Jan 17 '19 at 19:33
  • 1
    @Swordfish doesn't apply. The question is clear (albeit long) – SergeyA Jan 17 '19 at 19:35
  • This would be super easy to solve in C++, but I do not see a solution in C. I am also not sure if `memcpy()` into such structure is defined in C. (it is certainly undefined in C++) – SergeyA Jan 17 '19 at 19:37
  • @SergeyA Immutable is immutable. Even in C. Jes, the question might be "clear" but it is also unnecessary long. – Swordfish Jan 17 '19 at 19:39
  • @Swordfish it either is undefined in C or not. In C++ it is undefined because the class is not trivially copyable, but as far as I know, such concept doesn't exist in C. – SergeyA Jan 17 '19 at 19:40
  • note that `struct vector p = getCurrentPosition();` is the same as `*position = getCurrentPosition();`: both are assignments. – Jean-François Fabre Jan 17 '19 at 19:42
  • 1
    @Jean-FrançoisFabre first is initialization, not assignment. – SergeyA Jan 17 '19 at 19:43
  • `const float x;` uninitialized calls for some hacks afterwards. Trust your API users. If they want to change the values, well, good for them, as long as they don't change the data of your code: return constant _pointers_ on structures you have in memory, they won't be able to change them. Why do you want to create immutable structures? – Jean-François Fabre Jan 17 '19 at 19:47
  • 1
    @Jean-FrançoisFabre as someone coming from C++, I most certainly appreciate the immutable structs, which have a very honored place in type system. I am very curious about how this can be solved in C. – SergeyA Jan 17 '19 at 19:50
  • so duplicate of https://stackoverflow.com/questions/2219001/how-to-initialize-const-members-of-structs-on-the-heap that you mentionned? I think so. – Jean-François Fabre Jan 17 '19 at 19:51
  • @Jean-FrançoisFabre I think it is related, but not a duplicate. If that was a duplicate, then what would the answer to my question be? – Reinier Torenbeek Jan 17 '19 at 19:56
  • @Jean-FrançoisFabre My question is: is this usage of the const keyword appropriate in this way. So you are saying: yes, according to that possible duplicate? – Reinier Torenbeek Jan 17 '19 at 19:58
  • read about this: https://stackoverflow.com/questions/45418112/c-can-constant-class-data-be-optimized-out-of-class-by-compiler – Jean-François Fabre Jan 17 '19 at 20:15
  • this could be your duplicate : https://stackoverflow.com/questions/6703288/initializing-const-members-of-structs-in-c-c-compiler-dependent but it's c++ – Jean-François Fabre Jan 17 '19 at 20:16
  • How are the pointers that are passed to `getCurrentPositionAndVelocity` created? – KamilCuk Jan 17 '19 at 20:23
  • IMO having `const` members in a struct is a bad idea for many reasons, one of which is that you even have to ask questions like this one. It just makes life harder for everyone with no real benefit. Why does it matter to your API if someone stores the value in a local variable and then wants to modify the local variable? – M.M Jan 17 '19 at 23:28
  • @M.M The vector struct is not such a good example. I would apply this to hold values that would be used in subsequent API calls -- for example a GUID that potentially needs to be externalized as well. An immutable construct like this seemed like a nice middle ground between an opaque pointer approach and a regular struct. From there, I started to think that the immutability concept may be appropriate everywhere, not (yet) aware of the consequences. – Reinier Torenbeek Jan 18 '19 at 01:50
  • @SergeyA "I am very curious about how this can be solved in C" -- my conclusion: Using the `const`-qualified members does result in immutable structs but their usability is so limited (due to the problems described here, among others), that people refrain from using them. – Reinier Torenbeek Jan 18 '19 at 15:28

2 Answers2

3

TL;DR:

Is this usage of the const keyword in line with its intention?

No.


My question is whether this usage of the const keyword is in line with its intention, from the API user's perspective?

I'm not sure what the API user's perspective is. In fact, a design such as you propose seems likely to produce a larger diversity of user perspective than usual, because the apparent intended behavior is inconsistent with C language requirements.

And would there be any risks involved from a compiler perspective, in that this code may, strictly speaking, violate the read-only semantics as indicated with the const keyword?

Yes. Specifically,


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.

(C2011, 6.7.3/6)


Compilers may do all sorts of interesting things when UB occurs. Among the more plausible is that they may assume that some undefined behaviors do not occur. Thus, for example, when compiling a program that calls your getCurrentPositionAndVelocity() function, a compiler might assume that the function does not modify the const members of the structures provided to it. Or the attempt to modify them might actually fail. Or anything else, really -- that's what "undefined" means.

As an API user, what would you think of this approach or would you have any better suggestions?

I would think my API provider should be doing their best to provide an implementation that conforms to the language standard.

Additionally, I would wonder who you think you're protecting me from by making the structure members const, and why you think you know better than I do whether the structure members should be modified. I would also wonder why anyone could possibly think such protection was important enough to warrant all the many problems that attend const structure members.

As for better suggestions, how about just not making the members const? It will save grief for both you and your users.


With respect to the edit adding a reference to How to initialize const members of structs on the heap, the case discussed there is importantly different from the case considered here as far as the standard is concerned. Dynamically-allocated memory has no declared type, but may have an effective type based on what has been written into it and / or how it is accessed. The rules for this are presented in paragraph 6.5/6 of the standard, and you can find discussions of that in several answers elsewhere on SO, such as this one that you linked in comments.

The bottom line is that an object with allocated lifetime gets its effective type from the first data written into it, and that first write can be viewed as its effective initialization (though the latter term is not used in the standard). Subsequent manipulations must respect the effective type, including constraints arising from constness of the type itself or its members, recursively, pursuant to paragraph 6.5/7, colloquially known as the "strict aliasing rule".

Objects with static or automatic lifetime have their declared type as their effective type, and have initial value as specified by their initializers, if any. They, too, must be manipulated in a manner consistent with their effective types.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Your answer seems to contradict [this comment](https://stackoverflow.com/questions/2219001/how-to-initialize-const-members-of-structs-on-the-heap#comment36714481_2219034). What is your opinion on that? – Reinier Torenbeek Jan 17 '19 at 20:58
  • No, @ReinierTorenbeek, there is no contradiction with that comment. The case of dynamically allocated memory into which no value has yet been written (as that comment addresses) is significantly different in the eyes of the standard from the case you present and to which this answer responds. – John Bollinger Jan 17 '19 at 21:17
  • To your question: "why you think you know better than I do whether the structure members should be modified": my goal is to be as expressive as possible about the nature and intention of the type. Immutable or read-only structs/classes exist in some other languages, so there seems to be a place for them, no? – Reinier Torenbeek Jan 17 '19 at 21:55
  • You tell me, @ReinierTorenbeek. What purpose do you intend to be served in your C API by providing this structure with unmodifiable members instead of modifiable ones? Making instances immutable is a means to an end, not an end in itself. C has `const` for a purpose, and it is permitted to apply it to structure members, but doing so is not very idiomatic. In part, that's because `const` structure members cause significant practical problems. Is the purpose served worth the trouble in this case? – John Bollinger Jan 17 '19 at 23:05
  • Getting back to your quote from the C standard, I believe that does not apply here because my objects are not defined with a const-qualified type. They do have const members, but I am not convinced that is the same thing (but can not find a confirmation nor a denial). [This explanation](https://stackoverflow.com/a/54245386/1380680) seems to be the correct one, would you agree? – Reinier Torenbeek Jan 18 '19 at 01:36
  • @ReinierTorenbeek, that certain objects happen to be members of a larger aggregate does not make them non-objects. That you access them via an lvalue designating the aggregate does not alter the applicability of paragraph 6.7.3/6. I do agree with the answer you've linked, and it is fully consistent with this one and with my subsequent comments on it. Indeed, it says the same thing I do about why the case you present in your example there is importantly different from the case presented in your example here. – John Bollinger Jan 18 '19 at 14:28
  • Many thanks for you additional explanations, very educational for me. – Reinier Torenbeek Jan 18 '19 at 14:31
-1
memcpy(position, &p, sizeof *position);
memcpy(velocity, &v, sizeof *velocity);

You abuse the contract with the compiler. You declare something const and then you try to work it around. Extremely bad practice

Just do not declare struct members as const it you want to violate this agreement.

0___________
  • 60,014
  • 4
  • 34
  • 74
  • 2
    If you claim this is undefined behavior, you should provide some quote from Standard to back this claim. – SergeyA Jan 17 '19 at 19:39
  • @SergeyA this is a very bard programming practice and it will not pass any code review in any company - even if it not an UB. So your comment is irrelevant (and your DV also) – 0___________ Jan 17 '19 at 19:41
  • 2
    As a technical person, I do not accept general 'bad practice' as a reason. If the behavior is defined, I see nothing wrong with that. – SergeyA Jan 17 '19 at 19:43
  • @P__J__ Please take a look at [this answer](https://stackoverflow.com/a/2219034/1380680) to the question [How to initialize const members of structs on the heap](https://stackoverflow.com/questions/2219001/how-to-initialize-const-members-of-structs-on-the-heap), and the comments. (53 upvotes, 0 downvotes, 190K rep) – Reinier Torenbeek Jan 17 '19 at 19:46
  • 1
    There is no abuse. The code is either performant, achieving the goal and correct, or not. The only question I have is if the code is conformant, I would appreciate if you could provide some relevant information here. General talk of good and bad practice has no place in technical discussion. – SergeyA Jan 17 '19 at 19:48
  • @ReinierTorenbeek 10 years ago many things were considered OK. After those 10 years and discovering many silly errors caused by those "workarounds" the approach has changed. IMO this kind of abuse == bad programming. – 0___________ Jan 17 '19 at 19:48
  • const mens - it can be only assigned during the initalisation. All other silly methods abuse this. – 0___________ Jan 17 '19 at 19:50
  • @ReinierTorenbeek the question you mentionned looks very much like an "original" target for this question, which is a duplicate. – Jean-François Fabre Jan 17 '19 at 19:50
  • 1
    "You abuse the contract with the compiler." Can you explain what exactly is being abused and provide a sample that alleviates the problem? That would probably make your answer a lot more clear to the readers. – Mast Jan 17 '19 at 19:57
  • You promised to do not change const variables. And you break this contract by memcpy. Simple abuse. Just do not make it if you want to change it runtime. – 0___________ Jan 17 '19 at 20:02
  • 1
    Promised to **whom**? Compiler? It's heart will be broken, but it will survive. – SergeyA Jan 17 '19 at 20:04