5

In the following code, copyThing() is written so that its return value has the same type as its argument value:

extern void *copyThingImpl(const void *x);

#define copyThing(x) ((typeof(x)) copyThingImpl(x))

void foo(void);

void foo(void)
{
    char *x = "foo";
    char *y;

    y = copyThing(x);
    (void) y;
}

This uses the widely available typeof operator. This works fine and compiles without warnings (e.g., gcc -Wall -O2 -c test.c).

Now if we change x to a const pointer, we will run into light problems:

extern void *copyThingImpl(const void *x);

#define copyThing(x) ((typeof(x)) copyThingImpl(x))

void foo(void);

void foo(void)
{
    const char *x = "foo";
    char *y;

    y = copyThing(x);
    (void) y;
}

This causes a warning:

warning: assignment discards 'const' qualifier from pointer target type

which is correct, because (typeof(x)) is (const char *), so assignment to non-const y will drop the const.

My understanding is that the typeof_unqual operator that is new in C23 is supposed to be able to address this case. So I try

extern void *copyThingImpl(const void *x);

#define copyThing(x) ((typeof_unqual(x)) copyThingImpl(x))

void foo(void);

void foo(void)
{
    const char *x = "foo";
    char *y;

    y = copyThing(x);
    (void) y;
}

If I compile this with an appropriate C23-enabling option (e.g., gcc -Wall -O2 -std=c2x -c test.c), I still get the same warning, from various gcc and clang versions.

Am I using this correctly? What is the point of typeof_unqual if this doesn't work?

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
Peter Eisentraut
  • 35,221
  • 12
  • 85
  • 90

5 Answers5

2

The problem with your code is that it is not the variable (pointer) x that is constant. It is the string literal that is pointed to by the pointer that is constant.

const char *x = "foo";

If you will write for example

const char * const x = "foo";

then the qualifier const will be discarded from the pointer x in the declaration of the pointer y.

So you could write

void foo(void)
{
    const char * const x = "foo";
    const char *y;

    y = copyThing(x);
    (void) y;
}

In this case the expression copyThing(x) will not be a constant pointer.

That is typeof( x ) is const char * const and typeof_unqual( x ) is const char *.

You can write for example

typedef const char * T;

In this case const T is equivalent to const char * const and removing the qualifier const from the typedef name const T you will get const char *.

There is an example in the C23 Standard. There is declared an array like

const char* const animals[] = {
"aardvark",
"bluejay",
"catte",
};

and this declaration

typeof_unqual(animals) animals2_array;

declares an array like

const char* animals2_array[3];

that is an array of non-constant pointers to string literals.

By the way pay attention to that in C opposite to C++ string literals (by historical reasons) have types of non-constant character arrays though any attempt to change a string literal in C as in C++ results in undefined behavior.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
2

typeof_unqual(x) only removes the qualifiers that pertain to x itself, not the object pointed to by x if it is a pointer.

To remove the const from the pointed to type, you must dereference x in the macro:

#define copyThing(x) ((typeof_unqual(*(x)) *)copyThingImpl(x))

The above version works in recent versions of gcc and clang but requires explicit c23 standard and some extra flags, both of which are compiler specific.

chqrlie
  • 131,814
  • 10
  • 121
  • 189
1

This is basically just a new flavour of the old const char* ptr vs char*const ptr FAQ. Everything left of the * states the pointed-at type and everything right of the * belongs to the pointer variable ptr itself.

Therefore in case of const char* ptr, typeof_unqual(ptr) will leave you with const char* ptr. But if ptr had been const char*const ptr, we end up with const char* - the pointed-at type remains unaffected but qualifiers belong to the variable ptr itself were removed.

You could actually fix this expression to work as intended through (typeof_unqual(*x)*) copyThingImpl(x)) since we are allowed to add an asterisk after typeof/typeof_unqual. Explanation:

  • x is const char*.
  • *x is const char.
  • typeof_unqual(*x) is char.
  • typeof_unqual(*x)* is char*.
  • (typeof_unqual(*x)*) The outer parenthesis makes this a cast.

All "language-lawyering" aside, macros like these are probably not correct to use for any purpose. If the actual intention was to create type safety, then the macro should have been declared as:

#define copyThing(x) ( (char*) _Generic((*x), char: copyThingImpl)(x) )

The generic expression discards qualifiers (as per C17 I believe) - 6.5.1.1 "The type of the controlling expression is the type of the expression as if it had undergone an lvalue conversion". This means that the above macro will accept char*, const char* (and volatile char* etc...) and the array equivalents, but not pointers to other types, including void*.

If the code compiled because the correct type was provided, the result of copyThingImpl(x) is cast to char*, which in this example was the only accepted type.

Lundin
  • 195,001
  • 40
  • 254
  • 396
0

typeof_unqual only removes top-level qualifiers. So it will turn

const char * const

to

const char *

But in your example, const applies to the type pointed to, not the variable itself, so it's not a top-level qualifier.

Barmar
  • 741,623
  • 53
  • 500
  • 612
0

If you try to avoid a discard const qualifier warning, and you try to repair it by making a cast, you are running into serious trouble with your design, as you are breaking the type system which is not there but to help yo in localizing bad type uses.

A const qualifier is something added by you to let the compiler trying to modify or update some data. If you assign/pass as parameter that data to a non const target receiver, you will get the warning, because the final protected data is no longer protected. The compiler is just so kind to remind you. But if you tell the compiler please, just forget my absurd definition as I'm crazy and like to do things this way then, why to declare the data as const at all???

If you have some const * pointer data, and you want to assign/alias it, do it by declaring the aliasing variable as const also. Never use a cast.

I remember a special case in which I had to pass a const * pointer to free(3), but free has no const * specification in its definition (probably because it's assumed by the implementors that after free() has finished it's work, the released data --and so the pointer itself-- is not usable at all, and so, it will better implemented as void free(void *); than void free(const void *);, but this impedes you to call free on a const void * pointer, and you will get this kind of problem) (the use case is of no interest here, so I will not explain why I had to use const * on allocated memory that supposedly I had to later protect, after initialization)

I solved the problem by just a simple cast to (void *) before passing the pointer to free(3) but I'd preferred the implementors of free(3) to have wrapped internally the, then const void * parameter to a void * in case they preferred to implement some writing in the upto then non writable allocated memory.

In any case, if you are going to break the type system then just use a cast to (void *), or whatever your convenience type is, and then don't use const at all in your code.


If your function:

extern void *copyThingImpl(const void *x);

is, in some case, susceptible to return the original passed pointer value, then it should have been defined as:

extern const void *copyThingImpl(const void *x);

and internally change all char * definitions to const char *, instead. This is the correct, and typesafe way of doing what you want.

As a general rule, try to be as permissive in what you accept (do use const in parameters, only if you are not touching the data at all, but only then) and as strict as you can in what you return back (e.g. use const if for some reason the data you return cannot be changed, as if you have to return a pointer that was const) A const variable can be initialized with non const data (this will make the data stored not modifiable on the copy) but the reverse is not possible. This is checked by the compiler in every assignment or parameter passed to a function.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31
  • "If you try to avoid a discard const qualifier and you try to repair it by making a cast, you are running into serious trouble with your design." Generally this is very true but it's a well-known problem that C has lots of broken-by-design functions like `strstr` which takes a parameter which may or may not be const-qualified and then returns a pointer to that data which is not const. C23 actually tries to fix this ancient language design fault by inventing something called `QChar*`/`QVoid*`, which guarantees that the returned pointer has the same qualifiers as the one passed to the function. – Lundin Aug 17 '23 at 06:30
  • the most interesting is probably the one of `free()` which requires the pointer to be read-write (probably for internal implementation reasons) You are true, but it's a mistake to consider frivolously to solve problems making casts. – Luis Colorado Aug 17 '23 at 13:34
  • The returned pointer from `malloc` & friends is however never `const`-qualified. In practice there is no risk "casting away" a `const` in case the original data is not read-only. So while it might smell of bad design, `free((void*)ptr)` should always be well-defined no matter what type `ptr` might have. Whereas `strstr` can't know or assume anything about if the data is read-only or not. – Lundin Aug 17 '23 at 13:50