3

I'm currently working on a project where I often have to build linked lists of various C structs. Since I don't want to keep repeating myself setting next pointers, I wrote some helper templates, but soon found out that it falls apart if one of the next fields is a pointer-to-const.

My linked list elements look something like this:

struct WorkingElementType {
  void *pNext;
  /* stuff */
};
struct TroublesomeElementType {
  const void *pNext;
  /* stuff */
};

In reality, there are of course a lot more of these structs. My helper functions have to keep a pointer to the last element's pNext field in order to write to it when the linked list gets extended, so I went for a void **ppNext = &last->pNext. Unfortunately, that of course breaks down with TroublesomeElementType and its const void *pNext.

In the end, what I'd like to achieve is this:

void **m_ppNext;

/* In one function */
m_ppNext = &last->pNext;

/* In a different function, extending the list */
T *elementToAppend = ...;
*m_ppNext = elementToAppend;

I solved this by using a std::variant<void **, const void **> ppNext instead, but using a std::variant and std::visit just for a difference in constness that doesn't even affect the code's function feels like a bit of a waste.

That's why I'm wondering: Is it legal to use const_cast here to cast away const and stuff the const void ** into a void ** only for updating the pointed-to pointer? No const object actually gets modified, after all.

In other words: I'm not sure whether it's legal to alias const void* and void *. (My gut feeling says no, it's not legal because these are incompatible types, but I don't know for sure.)

The C++ standard in question is C++20.

Here's some simple example code:

#include <variant>


int g_i = 42;


/* This is legal */

void setIntPtr1(std::variant<int **, const int **> v) {
    std::visit([](auto& p) { *p = &g_i; }, v);
}

int testNonConst1() {
    int *i;
    setIntPtr1(&i);
    return *i;
}

int testConst1() {
    const int *i;
    setIntPtr1(&i);
    return *i;
}


/* But I'm not sure about this */

void setIntPtr2(int **p) {
    *p = &g_i;
}

int testNonConst2() {
    int *i;
    setIntPtr2(&i);
    return *i;
}

int testConst2() {
    const int *i;
    setIntPtr2(const_cast<int **>(&i)); // Is this legal?
    return *i;
}

On Godbolt, all of the various test... functions compile to the exact same assembly, but I don't know if testConst2 is legal C++.

I've found the following two existing questions:

  1. Is it legal to modify any data pointer through a void **
  2. Why isn't it legal to convert "pointer to pointer to non-const" to a "pointer to pointer to const"

However, both of them don't seem to quite answer my question. The first one deals with casting any T** to a void **, which is not what I'm doing; I'm just casting away constness. The second one asks why it's a compile error to convert a void ** to a const void **, but not whether interpreting the memory of a void * as a const void * and vice-versa (without actually overwriting a const object) would be a violation of the aliasing rules.

Jonathan S.
  • 1,796
  • 5
  • 14
  • Ask yourself this - if the compiler has decided that it's allowed to optimise a variable because it's const, to be something that's in ROM - is it valid to write to it via a pointer to a pointer to it? – UKMonkey Jan 12 '23 at 01:05
  • @UKMonkey None of these will ever be in a read-only section. All of the objects I'm linking together are non-const; I allocate them with `malloc` for API reasons (and then construct them with placement new). The problem is just that some of them have a `const void *pNext` instead of a `void *pNext` and I'd like to deal with them in a uniform way. Like I already said, no const object will ever be modified. I want to put a pointer to my non-const object in a `void *` or a `const void *`, but at runtime, I don't know which of the two it is. – Jonathan S. Jan 12 '23 at 01:09
  • 3
    Sure. As long as the object pointed to is not const, it is perfectly fine. – Peter - Reinstate Monica Jan 12 '23 at 01:11
  • `testConst2` does not crash; in fact, Clang 15 is able to resolve all of it at compile time. It turns it into a simple `return g_i;`, as intended. – Jonathan S. Jan 12 '23 at 01:13
  • Ah, I missed that. Yes, fine then. If you make g_i const, not fine. – Peter - Reinstate Monica Jan 12 '23 at 01:15
  • 2
    If the const object is never modified through a non-const pointer, it's okay, but the footgun is loaded and the safety is off. – Eljay Jan 12 '23 at 01:45
  • aside: If you changed `setIntPtr2(int **p)` to `setIntPtr2(const int **p)` then you're stating that it's a "pointer, to a pointer, to a const int" which means you no longer need to do any casting. – UKMonkey Jan 12 '23 at 02:07
  • @UKMonkey The implicit const-qualifier conversion works only on the outer-most pointer level. The call in `testNonConst2` would fail and again require an explicit cast. – user17732522 Jan 12 '23 at 04:15
  • It's obvious that reinterpreting as "more const" is always OK, otherwise you wouldn't be able to simply assign to "more const". Also that clearly implies that const has no effect on object representation (but type change might change representation, although it never does on "flat" arch). – curiousguy Feb 26 '23 at 00:03

1 Answers1

3

Yes, it is legal.

[basic.lval]/11:

If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined:

  • the dynamic type of the object [...]

T* and const T* are similar:

Two types T1 and T2 are similar if they have cv-decompositions with the same n such that corresponding Pi components are either the same or one is "array of Ni" and the other is "array of unknown bound of", and the types denoted by U are the same.

duck
  • 1,455
  • 2
  • 8
  • Your quote shows why `*p = &g_i` correctly writes the value "pointer to `g_i`" to the `int const*` object that the `int*` glvalue `*p` refers to. I would also reference https://timsong-cpp.github.io/cppwp/n4868/expr.const.cast#3 to reassure OP that the `const_cast` is valid and successfully results in a `int**` (`p`) that points to a `int const*`. – HTNW Jan 12 '23 at 02:29
  • @HTNW Try as I might, I cannot understand that section. – Peter - Reinstate Monica Jan 12 '23 at 03:39