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
  • 37,819
  • 6
  • 48
  • 88
  • 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

3 Answers3

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
  • 697,062
  • 78
  • 1,055
  • 1,465
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
  • 174,148
  • 38
  • 234
  • 367
  • 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
0

This code is safe if your platform correctly implements the IEEE754 standard.

In the standard, a sequence of bits of value 0 for both the exponent and the significant field is a representation of the 0.0 value.

If your platform uses a different standard then you have to be sure that a sequence of 0 bits is actually a double precision floating point representation of the 0.0 value.

auserdude
  • 891
  • 6
  • 17
  • 1
    Pointer conversions from `double*` to `const char**` are not well-defined so the code is not safe, regardless of floating point implementation. – Lundin Sep 01 '16 at 13:07
  • 1
    There is no pointer conversion from double* to const char* in the code. The pointer argument of the function is a void pointer. It is the void pointer that is converted to const char* pointer. Everything is explicit. There are no implicit conversions, the only grey area is the assignement of 0, but it's correct. As the sizes of the 2 types are checked (double vs const char*) the code is safe. Except for the 0.0 value representation which could be wrong in case the floating point is not the IEEE754. – auserdude Sep 01 '16 at 13:20
  • @Lundin, the dereferenced pointer is a `const char *`, not a `const char **`, AFAIK `const char *ptr = (const char *)&some_double;` is safe. – David Ranieri Sep 01 '16 at 13:49
  • 1
    @AuserDude It doesn't work that way, you can't just cast any random stuff to `void*` and think you can bypass all C standard rules of conversion and pointer aliasing that way. What matters is the _effective type_ of the object, as specified by 6.5/6. – Lundin Sep 01 '16 at 14:01
  • 1
    @AlterMann The effective type of the object pointed at by the void pointer is `double`. You then try to convert a pointer to that address to a pointer-to-pointer. 6.5/6 "The effective type of an object for an access to its stored value is the declared type of the object, if any. If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value." – Lundin Sep 01 '16 at 14:04
  • Could you explain exactly what's going to be unsafe in this code? Could you provide an example of wrong behaviour? – auserdude Sep 01 '16 at 14:04
  • 1
    @AuserDude See [What is the strict aliasing rule?](http://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule). – Lundin Sep 01 '16 at 14:05
  • _You then try to convert a pointer to that address to a pointer-to-pointer_: where? `*(const char **)` results in a `const char *`, and you can use a `const char *` as a generic pointer to any pointer type. – David Ranieri Sep 01 '16 at 14:07
  • I agree this is not the best code you can write, anyway the question is about safeness. As the size is tested, and a char pointer is used, honestly,I can't see your point. What kind of wrong behaviour do you expect by running this specific code? Char* can be used for this purpose. – auserdude Sep 01 '16 at 14:37