Union with const and non-const members

Viewed 240

I'm writing some library code that exposes a const pointer to users but during certain operations I need to change where this pointer points (behind the scenes switcheroo tricks). One idea I had to solve this problem without encountering UB or strict-aliasing violations was to use a union with a const member:

// the pointed-to objects (in production code, these are actually malloc'd blocks of mem)
int x = 0, y = 7;

typedef union { int * const cp; int * p; } onion;
onion o = { .cp = &x };
printf("%d\n", *o.cp);   //  <---------------------- prints: 0
o.p = &y;
printf("%d\n", *o.cp);   //  <---------------------- prints: 7

But I don't know if this is well-defined or not... anybody know if it is (or isn't) and why?


EDIT: I think I muddied the waters by mentioning I was building a library as lots of people have asked for clarifying details about that rather than answering the much simpler question I intended.

Below, I've simplified the code by changing the type from int* to just int and now my question is simply: is the following well-defined?

typedef union { int const cp; int p; } onion;
onion o = { .cp = 0 };
printf("%d\n", o.cp);   //  <---------------------- prints: 0
o.p = 7;
printf("%d\n", o.cp);   //  <---------------------- prints: 7
4 Answers

Every programming book I've had told me the following.

static const int x = 7;
int *px = (int *)&x;

is not defined, but

static int x = 7;
const int *px1 = &x;
int *px2 = (int *)px1;

is defined. That is, you can always cast away the const-ness if the originating pointer (here the &x) wasn't const.

Here I'm leaning on the lack of a contrary opinion from any quality source and not bothering to look up the standard (for which I'm not going to pay).

However you're trying to export something const that isn't const. That is actually valid. The language allows for

extern const * int p;

to be writable behind the secnes. The way to switch it out to the file with the definition doesn't see it const is to define it as int *p; and carefully not include the declaration in the file containing the defintion. This allows you to cast away the const with impunity. Writing to it would look like:

int x;

    *((int **)&p) = &x;

Old compilers used to reject extern const volatile machine_register; but modern compilers are fine.

If the interface is a const-declared pointer such as int *const (like you've indicated in your comment), then there's nothing you can do to change that that will not trigger UB.

If you're storing an int * somewhere (e.g., as a static int *ip;) and are exposing its address via a an int *const* pointer (e.g., int *const* ipcp = &ip;, then you can simply recast to back to (int**) (the original type of &ip from the example I gave) and use that to access the int* pointer.

I think this is undefined as per C11 6.7.3 (equivalent paragraph is in all versions of the standard):

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.

o.cp is undoubtedly an object defined with a const-qualified type.

The modification of o.p does seem to me to count as an attempt to modify o.cp , since that is exactly why we are doing it!

The Standard uses the term "object" to refer to a number of concepts, including:

  1. an exclusive association of a region of storage of static, automatic, or thread duration to a "stand-alone" named identifier, which will hold its value throughout its lifetime unless modified using an lvalue or pointer derived from it.

  2. any region of storage identified by an lvalue.

Within block scope, a declaration struct s1 { int x,y; } v1; will cause the creation of an object called v1 which satisfying the first definition above. Within the lifetime of v1, no other named object which satisfies that definition will be observably associated with the same storage. An lvalue expression like v1.x would identify an object meeting the second definition, but not the first, since it would identify storage that is associated not just with the lvalue expression v1.x, but also with the named stand-alone object v1.

I don't think the authors of the Standard fully considered, or reached any sort of meaningful consensus on, the question of which meaning of "object" is described by the rule:

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.

It would certainly make sense that if an object of the first kind is defined with a const qualifier, the behavior of code that tries to modify it would be outside the Standard's jurisdiction. If one interprets the rule as applying more broadly to other kinds of objects as well, then actions that modify such objects within their lifetime would also fall outside the Standard's jurisdiction, but the Standard really doesn't meaningfully describe the lifetime of objects of the second type as being anything other than the lifetime of the underlying storage.

Interpreting the quoted text as applying only to objects of the first kind would yield clear and useful semantics; trying to apply it to other kinds of objects would yield semantics that are murkier. Perhaps such semantics could be useful for some purposes, but I don't see any advantage versus treating the text as applying to objects of the first type.

Related