1

In our codebase, having precise management of memory, data was being passed around like so:

typedef struct {
  size_t const sz_size;
  size_t sz_len;
  int32_t * const restrict pData;
} S_ARR_T;

int32_t highLevelAlloc[DATA_SIZE];
S_ARR_T sArr = {
  .sz_size = DATA_SIZE,
  .sz_len = 0U,
  .pData = &highLevelAlloc[0]
};

void vLoadData(S_ARR_T * const psArr);
vLoadData(&sArr);

The problem was when we realized that the implied const-ness of the data in pData can still be modified even when the struct is const:

sArr.pData[0] = 0; // Demo
// Read-only function of data, no write privileges
void vProcessData(S_ARR_T const * const psArr){
  psArr->pData[0] = -1;
}
vProcessData(&sArr);
printf("%d\n", sArr.pData[0]); // -1

What data type patterns or designs would most allow us to keep this light-weight data wrapper idea, while ensuring const correctness / read-only passing?

What I tried: Making a second struct type definition, with all of it's pointer members' data const, a "const version" S_CARR_T of the struct. But it seems that casting from the S_ARR_T -> S_CARR_T to pass S_ARR_T to a function taking S_CARR_T is not allowed, casting non-scalar. (Relevant? C99 6.5.7, 6.3.2.1)

This only worked when making a whole new S_CARR_T variable and copying it over, like so:

S_CARR_T sCArr = {sArr.sz_size, sArr.sz_len, sArr.pData};
vProcessCData(sCArr);

Which could be wrapped up in a non-const -> const function; is this really the simplest way?

1
  • Would const int32_t * const restrict pData work? I think that says that pData is a const pointer to a const int, but it's been a while since I used C much and I made minimal use of const at the time. Apr 24 at 8:26
6

There is no particularly good solution. C's const is not inherited by pointed-to data. This allows you to express a fixed pointer to mutable memory, but that's not often what you want.

Somewhat reasonable approaches include:

  • a structurally equivalent struct that makes everything const, and a function to convert to const struct representation. This is what you've suggested. It is not insane and likely to be the best available solution in your context, despite being pretty ugly. Assuming the conversion function is inline, it will likely be compiled as a no-op.

  • keep S_ARR_T an incomplete type and only interact with it via functions that have an implementation that know its layout. This would involve a signature like this:

    int32_t* S_ARR_data(S_ARR_T* arr);
    int32_t const* S_ARR_data_const(S_ARR_T const* arr);
    

    Drawback: this is not a zero-cost abstraction and has extra runtime overhead, except perhaps with link-time optimization.

  • use C++, make those fields private, and add accessor methods. This is basically a less annoying variant of the above.

    class S_ARR {
      ...
    public:
      int32_t* data() { ... }
      int32_t const* data() const { ... }
    };
    

    Seriously, C++ is a very good language that gets rid of many of C's papercuts. The only reasons not to use C++ as a better C (even without the standard library) are political (someone higher up really likes C / dislikes C++) or due to more limited platform support, especially in an embedded context.

1
  • 1
    Thanks for the thoughts, +1. The biggest cost (although I do appreciate a no-op) is communication to other devs & (perceived) developer effort, which may be the downside of approach #2. As for #3, while I personally think that SW designed in/like C vs C++ is cleaner / better designed, my real reason is the safety-critical embedded controllers we target that require C for our safety certification process. Though, I do use C++ for testing, and if I could also use it for our production, then I could deal with the root of the issue multiple other ways...
    – JWCS
    Apr 25 at 5:00
5

You can't do this easily in C because constness is not transitive. Some people (including me) consider this to be a design flaw in the language, which has been fixed in newer languages such as D and Rust.

In a language like C without transitive const, the best you can do is hide the non-const pieces behind an opaque type and only expose them via functions that are colored by constness. So, no using psArr->pData; you need to write pData(psArr) instead. You might come up with something like this:

sArr.h

// incomplete type
typedef struct sArr S_ARR_T;

// const-colored accessor functions
int32_t const *pcData(S_ARR_T const *p);
int32_t *pData(S_ARR_T *p);

sArr.c

// implementation, hidden from users
struct sArr {
  size_t const sz_size;
  size_t sz_len;
  int32_t * const restrict pData;
};

// accessor definitions
int32_t const *pData_const(S_ARR_T const *p) {
    return p->pData;
}

int32_t *pData(S_ARR_T *p) {
    return p->pData;
}

Client code

#include "sArr.h"

// Read-only function of data, no write privileges
void vProcessData(S_ARR_T const *psArr) {
    pData(psArr)[0] = -1; // warning: passing argument 1 of 'pData' discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
}

This does mean that you have to wrap basically the entire API in largely redundant functions. (As amon's answer points out, this is streamlined in C++ because that language has visibility controls built in.) However, I still prefer this option to making a parallel struct S_CARR_T, because:

  • If your API functions accept pointers, interleaving const and non-const calls entails having two copies of "the same" value that can get out of sync. (Consider what happens if you make a copy of sArr to call a const-taking function, then mutate sArr, then call another const-taking function on the old sCArr without updating it to the new state.)

  • If you rewrite the API to take structs by value instead to avoid the above problem, then every call instead copies the entire struct. The optimizer may be able to handle this, but probably not without aggressive inlining of all functions involved (not just the conversion itself). Since you're going to be relying on LTO (link-time optimization) anyway, that is not a reason to avoid using an incomplete type.

However, I doubt that performance is an issue either way, assuming a reasonably modern compiler.

7
  • When you want transitive const and cannot get it, it is natural to think the absence a design-flaw. When you don't want it and aren't forced into it, it is natural not to recognize your blessing. A much more complex language with classes, access-protection and the like would allow you to have both, but one would still be the default. Apr 23 at 22:32
  • You could have just declared const int32_t * const pData. Const pointer to const ints.
    – gnasher729
    Apr 24 at 9:41
  • 1
    @gnasher No, that would just make the pointer unconditionally const. What OP wants is for the pointee to inherit the constness of the structure (transitive const).
    – trentcl
    Apr 24 at 10:34
  • Thanks for your thoughts, & expansion of the opaque type example, +1. I was a bit skeptical of the work, after only reading anon's answer, but for a small enough structs, I could see the value in simplicity.
    – JWCS
    Apr 25 at 5:09
  • 1
    @JWCS You need more than that for the conversion to be a no-op: you need the compiler to recognize that f_of_S_CARR_T does not mutate its argument, which probably means that function (not just sCArr) has to be inlined (the presence of a const in the argument list has no effect on optimizations in the caller), and you need to assume the C ABI passes sCArr implicitly by reference rather than on the stack or in registers. Compiler Explorer suggests gcc is not able to elide the copy, even with optimizations (on bottom; opaque type example on top).
    – trentcl
    Apr 27 at 16:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.