Search code examples
c++arrayspointerstypedefdereference

Is there a need to dereference when performing a memcpy using pointer to typedef fixed length array? Why or why not?


Ok - so I'll preface this by saying I'm not entirely sure how to describe the question and my current confusion, so I'll do my best to provide examples.

Question

Which of the two approaches to using the typedef-ed, fixed-length array in a memcpy call (shown below in "Context") is correct? Or are they equivalent?

(I'm starting the think that they are equivalent - some experimentation under "Notes", below).

Context

Consider the following typedef typedef uint8_t msgdata[150]; and the library interface const msgdata* IRead_GetMsgData (void); .

In my code, I use IRead_GetMsgData and memcpy the result into another uint8_t buffer (contrived example below).

//Included from library:
//typedef uint8_t msgdata[150];
//const msgdata* IRead_GetMsgData (void);

uint8_t mBuff[2048];
void Foo() {
    const msgdata* myData = IRead_GetMsgData();
    if(myData != nullptr) {
        std::memcpy(mBuff, *myData, sizeof(msgdata));
    }
}

Now, this works and passes our unit tests fine but it started a discussion between our team about whether we should dereference myData in this case. It turns out, not dereferencing myData also works and passes all our unit tests

    std::memcpy(mBuff, myData, sizeof(msgdata)); //This works fine, too

My thought when writing the memcpy call was that, because myData is of type msgdata*, dereferencing it would return the pointed-to msgdata, which is a uint8_t array. E.g.

    typedef uint8 msgdata[150];
    msgdata mData = {0u};
    msgdata* pData = &mData;     
    memcpy(somePtr, pData, size);  //Would expect this to fail - pData isn't the buffer mData.    
    memcpy(somePtr, *pData, size); //Would expect this to work - dereferencing pData returns the buffer mData
    memcpy(somePtr, mData, size); //Would expect this to work - mData is the buffer, mData ==&mData[0]

I've tried searching for discussion of similar questions but haven't yet found anything that felt relevant:

The last one in that list felt most relevant to me, as the accepted answer nicely states (emphasis mine)

[this form of typedef is] probably a very bad idea

Which, having now tried to understand what's actually going on, I couldn't agree with more! Not least because it hides the type you're actually trying to work with...

Notes

So after we started thinking on this, I did a bit of experimentation:

typedef uint8_t msgdata[150];
msgdata data = {0};
msgdata* pData = &data;
int main() {
    printf("%p\n", pData);
    printf("%p\n", *pData);
    printf("%p\n", &data);
    printf("%p\n", data);
    
    return 0;
}
Outputs:
0x6020a0 
0x6020a0 
0x6020a0 
0x6020a0

And if I extend that to include a suitable array, arr and a defined size value, size, I can use various memcpy calls such as

    std::memcpy(arr, data, size);
    std::memcpy(arr, pData, size);
    std::memcpy(arr, *pData, size);

Which all behave the same, leading me to believe they are equivalent. I understand the first and last versions (data and *pData), but I'm still unsure of what is happening regarding the pData version...


Solution

  • This code is, IMO, plain wrong. I'd also accept the alternative view "the code is very misleading"

    //Included from library:
    //typedef uint8_t msgdata[150];
    //const msgdata* IRead_GetMsgData (void);
    
    uint8_t mBuff[2048];
    void Foo() {
        const msgdata* myData = IRead_GetMsgData();
        if(myData != nullptr) {
            std::memcpy(mBuff, *myData, sizeof(msgdata));
        }
    }
    

    When you dereference *myData, you mislead the reader. Obviously, memcpy requires a pointer to a msgdata, so the dereferencing star is not needed. myData is already a pointer. Introducing an extra dereference would break the code.

    But it doesn't... Why?

    That's where you specific use case kicks in. typedef uint8_t msgdata[150]; msgdata is an array that decays into a pointer. So, *msgdata is the array, and an array is(decays into) a pointer to its beginning.

    So, you could argue: no big deal, I can leave my extra * in, right ?

    No.

    Because someday, someone will change the code to:

    class msgdata
    {
        int something_super_useful;
        uint8_t msgdata[150];
    };
    

    In this case, the compiler will catch it but, in general, an indirection level error might compile to a subtle crash. It would take you hours or days to find the extraneous *.