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 memcpy
s 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?
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 const
ness 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.
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.