1

Is it good practice to access a pointer variable by dereferencing a pointer to a pointer, which points to a different type or void? Could this break strict aliasing rules? C and C++ have some differences in aliasing rules. In this question we focus on C++. The other question considering C can be found here. In the following example a double* is accessed as a void*.

int create_buffer(void** ptr, ...)
{
    *ptr = malloc(...);
    ...
}

int main(void)
{
    double* buffer;

    // The problematic code is here, double**
    // is coerced to void**, which is later
    // dereferenced by the function
    create_buffer(reinterpret_cast<void**>(&buffer), ...);
    ...
}

If this is causes UB, what about the following?

// process A
int* p;  ...
printf("%p", p); // UB?

// process B
int* p;
scanf("%p", &p); // UB?

This looks like a bad example, but what if two processes talk to each other through pipes, and eventually one process passes a pointer to globally allocated memory to the other process.

cmdLP
  • 1,658
  • 9
  • 19
  • 2
    One aspect that is frowned upon in the C++ and C communities here is conflating the languages. You are in fact asking about behavior under two entirely different specifications. There is a whole lot of wrong here in C++, even without the aliasing issues. You need to pick *one* tag, or narrow your question down. "Good practices" will differ **a lot** between C and C++. – StoryTeller - Unslander Monica May 18 '19 at 14:27
  • please pick one language. Also it would help if you could explain why you are using `void**` – 463035818_is_not_an_ai May 18 '19 at 14:29
  • This question is answered for C++, should I reask the question with a focus on C? @StoryTeller – cmdLP May 18 '19 at 14:33
  • @cmdLP - I see no problem with asking about separately it in a C context. Two good questions are better than a single broad one :) – StoryTeller - Unslander Monica May 18 '19 at 14:40
  • @StoryTeller I just did that, with recursively referring to each other. – cmdLP May 18 '19 at 14:55

2 Answers2

2

Is it good practice to access...

No. void* is not the go to type for polymorphism and reuse in C++, even without considering the aliasing issues in your original code. With a rich template mechanism available you can make your code strongly typed and safer to boot. The obvious improvement is to use templates, and type safe allocation:

template<typename T>
int create_buffer(T** ptr, ...)
{
    *ptr = new T[...];
    ...
}

But to go off on a tangent, this is still not how good C++ will look like. Good C++ is about managing complexity correctly. And tracking a buffer is a complex task. The good C++ approach is to not do it by hand, but to encapsulate it in a dedicated class (template). In fact, this is such a common task, that the standard library provides a solution.

The good practice is to use std::vector<double> instead of the buffer creation function. A class template for a type generic task will often beat any use of void*. That will avoid any aliasing issues entirely, since the correct type is always used.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • I am currently writing a library, which should be usable with both C and C++. In C there are no templates, typeless code using `void*` is common in C. Just look at the memory allocation functions or `qsort` in C. In C++ there are templates like `std::vector` or `std::sort`, which allow more safer code. But templates might cause the binary to grow drastically in size. There is the thing of *thin templates*, which wrap templates around typeless code, but these use constructs like `void*` again. – cmdLP May 18 '19 at 15:09
  • 1
    @cmdLP - Causing the binary to grow drastically in size is something that may have been true in the 90's. With COMDAT folding / ICF optimizations, that is hardly the case anymore. While separation of concerns is good, and raw memory should be managed in a dedicated component, don't optimize prematurely based on aged information. – StoryTeller - Unslander Monica May 18 '19 at 15:18
  • This is true. Compilers are getting smarter in this kind of optimizations. But in embedded systems, where memory (both RAM and ROM) is a precious resource, the optimizations done by the compiler might not be enough or there might not even be a compiler for such a platform, which does these optimizations – cmdLP May 18 '19 at 15:29
1

This is UB as you are assigning a double* variable as if it was a void* variable (It has the same effect as reinterpret_cast<void*&>(buffer) = malloc(...); in C++. This would be OK on most systems as void* and double* are usually exactly the same, but it is still UB, so might not work on all implementations).

A solution would be to assign to a different variable and then reassign it:

int main(void)
{
    double* buffer;

    {
        void* result;
        create_buffer(&result, ...);
        buffer = (double*) result;  // Cast needed for C++
    }
    ...
}
Artyer
  • 31,034
  • 3
  • 47
  • 75
  • Is it also a problem in C? – cmdLP May 18 '19 at 14:27
  • @cmdLP Yes it is, I just used `reinterpret_cast` as an example because it was more clear – Artyer May 18 '19 at 14:27
  • "best solution" ? it would be better not to use `void**` in the first place. To create a buffer of `double`s why not simply use a buffer of `double`s ? – 463035818_is_not_an_ai May 18 '19 at 14:28
  • @formerlyknownas_463035818 without the context it is hard to tell (Could be a general function), but noted – Artyer May 18 '19 at 14:29
  • @formerlyknownas_463035818 @Artyer In the code I am currently writing it should be indeed a general function, which has some allocation behavior as `std::vector`. It always (re)allocates an amount which is a power of 2 and does some other checks like overflow checking, if `size*element_size` overflows. – cmdLP May 18 '19 at 14:45
  • Is it really a problem to access a `double*` through a `void*`? `printf` itself interprets any passed pointer parameter as `void*` internally when the format specifier is "%p" (by `va_arg(v, void*)`). And when scanf writes to a buffer the pointer is loaded by `va_arg(v, int*)` for example. There is no distinction between types when passing pointers as an variadic argument. Could this either be an errorneous implementation of `scanf` or is the write the problem or does the read also cause UB? This would also mean that any `int* p; scanf("%p", &p);` is also UB – cmdLP May 18 '19 at 15:52
  • @cmdLP Yes those are UB. But in most cases it will work because `int*` will *usually* be the same as `void*` in layout/representation. If you compile with warnings it will warn you that you used int* instead of void*. See https://stackoverflow.com/a/24867850 – Artyer May 18 '19 at 15:58
  • @cmdLP even if you avoid aliasing or other causes of UB, your are still not type safe, which often leads to runtime errors that you otherwise could easily catch at compile time. If this is C++ you really shouldnt use `void*` for this task – 463035818_is_not_an_ai May 18 '19 at 22:44