2

I want to set the value of the (dereferenced) passed variable to NULL if it is a const char * and to 0 if it is a double, assuming that NULL is defined as (void *)0 and sizeof(const char *) == sizeof(double), is this code safe? is there a better approach to achieve the same?

If not, please, don't suggest to use unions, I am stuck with void * (building an interpreter) and I can not pass the type as parameter, I only need those 2 type (const char * and double).

#include <stdio.h>
#include <string.h>

static void set0(void *param)
{
    if (sizeof(const char *) == sizeof(double)) {
        *(const char **)param = 0;
    }
}

int main(void)
{
    const char *str = "Hello";
    double num = 3.14;

    printf("%s\n", str);
    printf("%f\n", num);
    set0(&str);
    if (str != NULL) {
        printf("%s\n", str);
    }
    set0(&num);
    printf("%f\n", num);
    return 0;
}
David Ranieri
  • 39,972
  • 7
  • 52
  • 94
  • The *right* thing to do is to remember which one it is. You know `sizeof(const char *) == sizeof(double)` isn't true on most 32-bit architectures? – user253751 Sep 01 '16 at 12:11
  • How do you expect this to run on a 32-bit system, where (typically) a `double` will be twice as large as a pointer? You need more information. – unwind Sep 01 '16 at 12:12
  • @unwind: Notice the `if (sizeof(const char *) == sizeof(double))` – David Ranieri Sep 01 '16 at 12:12
  • @AlterMann So on 32-bit systems it doesn't set it to 0 or NULL. So on 32-bit systems, it doesn't work because it doesn't do what it's supposed to. – user253751 Sep 01 '16 at 12:14
  • 1
    Wouldn't `static_assert(sizeof(const char*) == sizeof(double), "Incompatible target machine");` be more efficient? (or can the compiler optimize the `if` to a compile-time thing in this scenario?) – Michael Sep 01 '16 at 12:18
  • @Michael, but `static_assert` is a C++ artifact, isn't it? – David Ranieri Sep 01 '16 at 12:20
  • 1
    No, it's a C11 thing. But it also works in C++, at least with g++ 5.2.0. – Michael Sep 01 '16 at 12:20
  • @Michael, then yes, it is useful, but the question is, can I assume that this code sets `0` or `NULL` in a safer mode? – David Ranieri Sep 01 '16 at 12:22
  • @AlterMann You're trying to write to a string literal location. It will trigger an undefined behavior. – Delights Sep 01 '16 at 17:47

2 Answers2

5

In order for this to be safe even on platforms where sizeof(double) is the same as sizeof(const char*) one other condition must be in place: the way the system represents doubles must interpret bits of a NULL pointer as 0.0.

Although this is true for many platforms, because both NULL and 0.0 are represented as sequences of zero bytes of identical length, the standard by no means requires this to be true. Zero double may have a representation different from what it is in IEEE-754. Similarly, NULL pointer is not required to be represented as a zero (although the compiler must ensure that zero comparison of NULL pointer succeeds). Therefore, you end up with rather unportable code.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
3

In C11 you could simply do this:

#define set0(param) \
  if (sizeof(const char *) == sizeof(double)) \
  { param = _Generic((param), double: 0.0, const char*: NULL); }

...

set0(str);
set0(num);

or if you will (uglier macro, prettier call):

#define zero(param)                                     \
  (sizeof(const char *) == sizeof(double)               \
   ? _Generic((param), double: 0.0, const char*: NULL)  \
   : param) 

...

str = zero(str);
num = zero(num);

This also has the advantage of type safety, which your void* function lacks.

Otherwise, you would be out of luck. Pointer conversions from double* to const char** are not well-defined. The rules of pointer aliasing allow pointer conversions from type* to const char* but not to const char**.

In order to write this code with a function taking a void*, you would have to add an additional type information parameter.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • Unfortunatelly, I have no access to the object itself, variables are always passed as pointers `(void *)` to the interpreter, in other words, I can not use `_Generic` because at the moment to call `set0` they are pointers (not primitive types), an example: `if (get(this), set(this, format("%0*.0f", size(this), val(get(this)))), /* else */ set(this, 0))`, in the second `set` I don't know the type of `this`, time to redesign all, thank you anyway. – David Ranieri Sep 01 '16 at 13:29
  • _The rules of pointer aliasing allow pointer conversions from type* to const char* but not to const char**._. But there is not such conversion, the passed element is a `const char *` dereferenced in `*(const char **)param = 0;` – David Ranieri Sep 01 '16 at 13:59
  • @AlterMann Here `*(const char **)param` the _effective type_ of the object pointed at by param is `double`. You are essentially writing `(const char**)(void*)&double_obj` which is equivalent to `(const char**)&double_obj` – Lundin Sep 01 '16 at 14:08
  • You forget the dereference operator, `*(const char **)` results in a `const char *` , not in a `(const char**)&`, in fact `(const char **)&param = 0;` raises `error: lvalue required as left operand of assignment` – David Ranieri Sep 01 '16 at 14:27
  • Ok Lundin,I have spent several hours but I get your point: I am dereferencing an invalid cast (`double *` --> `const char **`), you mean the dereference is applied after the (invalid) conversion, isn't it? – David Ranieri Sep 01 '16 at 16:33
  • 1
    @AlterMann Yes. The cast and the unary * operator have the same precedence, but right-to-left associativity, meaning that the cast happens before the de-reference. – Lundin Sep 01 '16 at 17:57
  • Thank you Lundin, now I understand, and excuse me for being a little obtuse. – David Ranieri Sep 01 '16 at 22:01