1

I can take a T*& from a T*. Now I need to store my T* in a type-erased way, more specifically as a void*. Can I take a T*& from a void* ? (knowing, of course, that my void* does point to Ts)

Example:

#include <iostream>
#include <cstdlib>
#include <numeric>

int main() {
  int n = 10;
  void* mdbuf = malloc(n*sizeof(double));
  double* mdarr = (double*)mdbuf;
  std::iota(mdarr,mdarr+n,0.); // initialize the memory with doubles

  // solution 1: works but not what I want since I want to refer to the type-erased mdbuf variable
  double*& mdarr_ref = mdarr; // ok, now mdarr_ref refers to variable mdarr
  
  // solution 2: does not compile
  double*& mdbuf_ref = (double*)mdbuf; // error: cannot bind non-const lvalue reference of type 'double*&' to an rvalue of type 'double*'

  // solution 3: compiles and work but I want to be sure this is not out of pure luck: is it undefined behavior?
  double*& mdbuf_ref = (double*&)mdbuf; // we would like mdbuf_ref to refer to variable mdbuf. It compiles...
  std::iota(mdbuf_ref,mdbuf_ref+n,100.);
  for (int i=0; i<n; ++i) {
    std::cout << mdbuf_ref[i] << ", "; // ...does what we want in this case... is it valid however?
  }
}

Edit: Maybe one way to look at it is the following:

double d;
void* v_ptr = &d;
double* d_ptr = (double*)v_ptr; // (1) valid
double& d_ref = d; // (2) valid
double& d_ref2 = (double&)d; // (3) valid? Should be the same as (2) ?
double*& d_ref3 = (double*&)v_ptr; // (4)

The question is: is (4) valid? If (1) and (3) hold, it is just chaining both, so I expect it to be valid, but I would like some evidence of it

Bérenger
  • 2,678
  • 2
  • 21
  • 42
  • 5
    Do you have a use case where the reference would be meaningful? – molbdnilo Jul 12 '20 at 13:16
  • @molbdnilo yes: if I reallocate memory, and make mdbuf point to it, I want mdbuf_ref to refer to the new value of mdbuf in order to access the new memory. – Bérenger Jul 12 '20 at 13:19
  • why you don't take it from `mdarr`? – apple apple Jul 12 '20 at 13:25
  • @appleapple Because what I store is (wrapped) void* to different types in the same database. It is because of type erasure – Bérenger Jul 12 '20 at 13:30
  • 1
    No this is not legal. On word-addressible systems, `sizeof(void*) != sizeof(double*)`. A `void*` consists of an address + byte index, not just an address. – Raymond Chen Jul 12 '20 at 13:30
  • 1
    @RaymondChen I am very surprised. Do you have a source ? What do you mean by not legal ? Implementation defined, or undefined behavior? On all systems I care about `sizeof(void*) == sizeof(double*)` – Bérenger Jul 12 '20 at 13:33
  • @Bérenger is it necessary to be a reference? or you can use a wrapper object? – apple apple Jul 12 '20 at 13:41
  • Presumably a laundered reinterpret cast would work? (I.e. take the address of the void pointer, cast it to a pointer to pointer to double, launder that, then dereference.) You may have to save the void pointer first and then assign (or maybe even placement-new??) it back through the new reference, though. That assumes a void pointer indeed is big enough to fit a double pointer in its storage. And you can’t use it as a void pointer afterwards. – HTNW Jul 12 '20 at 13:42
  • moved from my comment on a removed answer, I think it may help make this question more clear (@Bérenger have confirmed this to be true): ** if I got correctly, what OP want is a `double*&` somehow link to a `void*`. (i.e. a `void*&` with auto type cast ...?) ** – apple apple Jul 12 '20 at 13:43
  • @appleapple If the code I have shown is not legal I will end up doing that, but I don't want it to be unnecessarily complicated :D – Bérenger Jul 12 '20 at 13:44
  • @appleapple Thanks. Also I edited the post to make the candidate solution (#3) more clear – Bérenger Jul 12 '20 at 13:50
  • @HTNW I think it would end up being like solution 3, but more complicated. Once again, solution 3 kind of "work", I am just not conviced it is legal – Bérenger Jul 12 '20 at 13:51
  • 1
    Nowhere in your example you have an object of type `double` (your `T`, I presume). Just because you hold space for it doesn't mean you have an object. So, although you are allowed to take a reference to a `double*` if you create such an object (as in solution 1), you will never be able to do anything with that pointer. (language lawyer tag) – bitmask Jul 12 '20 at 13:58
  • @bitmask Yes I can. That is what I do later on with iota: I assign to it. In reality, I have an object (well 10 actually) in a partially formed state (EOP parlance). – Bérenger Jul 12 '20 at 14:04
  • 1
    @Bérenger Why do you insist on mallocing the thing anyway? If you do it properly with `new` (or even use `std::array` or `std::vector`) you'd already have a valid object and a pointer to it with the correct type. – bitmask Jul 12 '20 at 14:09
  • @bitmask This is correct, I could `new` it. But in the end I want to type-erase the object to store it, because I actually have different types and I want to store them in a complex structure where I want only one type (e.g. `void*` or any type-erased wrapper) – Bérenger Jul 12 '20 at 14:28
  • @Bérenger Have a look at [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant) or [`std::any`](https://en.cppreference.com/w/cpp/utility/any), then. – bitmask Jul 12 '20 at 14:40
  • A famous word-addressable system is the PDP-10 whose smallest unit of storage is the 36-bit word. An int*` is the address of one of the 36-bit words. A `char*` (and therefore also a `void*`) was an `int*` combined with an offset that identified a 9-bit byte within the word. A reference to one cannot be used as a reference to the other. To type-erase your `double*`, store it internally as a `void*` and cast the value (not the reference) to `double*` when you use it, like you did in solution 1. For realloc, do `mdbuf = realloc(mdbuf, new_size * sizeof(double))`. – Raymond Chen Jul 12 '20 at 17:51
  • (4) is not legal because you removed the lvalue `d_ptr` from the chain. The assignment from `v_ptr` to `d_ptr` is a **conversion**. It's the same as this: `double d; int i = (int)d; int& i_ref = (int&)i;` is legal. But you cannot collapse the last two lines into `int& i_ref = (int&)d;` – Raymond Chen Jul 12 '20 at 23:10

2 Answers2

1

I'm going to take your second example and rewrite parts of it using aliases to better illustrate what you're asking for.

using V = void*;
using K = double*;

double d;
V v_ptr = reinterpret_cast<V>(&d);
V &v_ptr_ref1 = v_ptr; //Refers to the `V` object denoted by `v_ptr`.
K d_ptr = &d;
K &d_ptr_ref1 = d_ptr; //Refers to the `K` object denoted by `d_ptr`.
V &d_ptr_ref2 = reinterpret_cast<V&>(d_ptr);

So, we have two types: K and V. In the last line, we initialize a reference to a V using an object of type K. So d_ptr_ref2 is initialized to reference an object of type K, but the type of the reference is V.

It doesn't matter if they are "just" pointer types. In C++, pointers are object types and they follow all the rules of any other object type.

C++'s strict aliasing rule forbids accessing an object of one type through a glvalue (like a reference) of a different type, outside of certain very specific circumstances. The specific exceptions vary slightly from version to version, but there is no version of C++ where void* and double* are an exception.

Attempting to access d_ptr_ref2 means that you're accessing an object of type K through a reference of an unrelated type V. That violates strict aliasing, thus yielding undefined behavior.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
0

Your solution 1 is the only answer; you can’t lie about the pointer type itself. (That question is about C, but the rules for C++ references are equivalent.)

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • 1
    Do you have a link where C++ rules for references are equivalent to C rules for pointers? – Bérenger Jul 12 '20 at 19:06
  • @Bérenger: They’re not equivalent in *all* cases, but the simple “don’t lie” rule is [basic.lval]/11. – Davis Herring Jul 12 '20 at 20:47
  • I don't understand why you say "don't lie". mdbuf does point to a `double` type (even though it is not known be the type system), so casting it to a `double*` is not lying, it is well defined. Moreover, `double d; double& d_ref = (double&)d` is also ok (I guess, we actually don't need to be that explicit). The question is then whether chaining the two together is fine. – Bérenger Jul 12 '20 at 21:37
  • @Bérenger: "*mdbuf does point to a `double` type*" No, it does not. It points to a blank piece of memory. At no point have you done any of the things the C++ standard requires you to do in order to put a `double` there. – Nicol Bolas Jul 12 '20 at 21:56
  • @NicolBolas Ok, I edited the post to make it point to doubles – Bérenger Jul 12 '20 at 21:59
  • The lie is that a bunch of memory (that hold a `void*`) is being used as a `double*` even though no `double*` was ever put there. You are therefore using a `double*` before its lifetime has started. The pointed-to memory is not the issue. It's the pointer itself. – Raymond Chen Jul 12 '20 at 22:02
  • @RaymondChen I edited that to clarify that the void* is really pointing to double. But even if the memory is not (yet) filled with doubles, I can still see it as a pointer of double in the case I want to *assign* double to the memory. – Bérenger Jul 12 '20 at 22:08
  • @RaymondChen: While a `double*` *could* be put in that space at another time (via placement new, perhaps), you don’t have a pointer to that hypothetical object (out of lifetime or otherwise). As such, this is a simple aliasing violation. – Davis Herring Jul 12 '20 at 22:10
  • @Bérenger: C++20 does finally specify that objects can magically be created by `malloc` and other similar means, but this isn’t about converting `void*` to `double*` but rather `void*&` to `double*&` which is quite different (albeit very similar to the equally invalid `void**` to `double**` conversion). You **can** do either via `reinterpret_cast`, but it’s undefined behavior to use the result other than to cast it back to the real type. – Davis Herring Jul 12 '20 at 22:13
  • The malloc is a red herring. You are taking the `sizeof(void*)` bytes that currently hold a `void*` and saying "Okay, now they hold a `double*`. This is legal, provided the first thing you do is write a `double*` to it, but instead you try to read a `double*` from it, which is not allowed since there is no `double*` there yet. – Raymond Chen Jul 12 '20 at 23:04
  • @RaymondChen: `void *p; reinterpret_cast(p)=nullptr;` has undefined behavior, even though it’s a write and even in C++20 (because there’s no implicit object creation in this case). – Davis Herring Jul 12 '20 at 23:11
  • @DavisHerring My mistake. The rules in **[basic.life]** are not exactly the most obvious. I thought writing to it would cause the lifetime to begin because that establishes completion of initialization, and it also implicitly terminates the lifetime of the `void*` ("A program may end the lifetime of any object by reusing the storage which the object occupies.") – Raymond Chen Jul 12 '20 at 23:48