1

The question:

I have some code executing which is very unsafe. My question is, does this code trigger undefined behavior or is it 'correct' (= behavior fully defined).

Some context:

I have a program which has shown strange bugs/crashes in the past, which repeatedly also vanish again without reason. I have identified some pieces of (pointer) logic as a potential source of these bugs. Still, the bugs can also be caused by other part of that code. The bugs typically happen in Release builds, and not in Debug builds in case that matters.

I have taken these lines of (pointer) logic code and created a clear minimum-working example. This minimum-working example runs without problems which I know is no conclusive proof that no undefined behavior is happening. Therefore, I ask if someone sees if some 'C++ rules' are broken (= results in undefined behavior).

I build and develop this in Visual Studio 2019 and/or 2022.

I know this code contains very bad patterns. However, this is a minimal working example and in the actual code, many things are out of my control so I cannot simply change much of it. For now, I just want to learn if certain pointer operations result in defined or undefined behavior.

In case one or more operation do trigger undefined behavior, I would be grateful if someone can point out which operations they are and why they trigger undefined behavior.

The code:

Main program, minimum_working_example.cpp:

#include <iostream>
#include <sstream>
#include "CppObject.h"

int main()
{
    /* Create a shared pointer on the heap.
     * The lifetime of this shared_ptr is managed by some managed C# code which wraps this C++ code.
     * That is why the shared_ptr is created on the heap, so C# can manage the lifetime of the pointer and therefore the C++ object.
     */
    std::shared_ptr<void>* regularPointerToSmartPointer = new std::shared_ptr<void>(); // Create a shared pointer on the heap, and store a regular pointer pointing to it.

    // Create an CppObject and store it in the shared_ptr<void> (note that this should add a reference to the correct type deleter into the shared_ptr).
    int someValue = 5; // Some value stored in the CppObject, just for illustration.
    *regularPointerToSmartPointer = std::make_shared<CppObjectStuff::CppObject>(someValue); // Create an object on the heap, create a smart pointer pointing to it, and store this smart pointer on the heap at the memory reserved in the previous step.

    // Just get the raw pointer to object.
    void* regularPointerToObject = (*regularPointerToSmartPointer).get();

    // Convert pointer to string.
    std::stringstream ss1;
    ss1 << std::hex << regularPointerToObject;  // Pointer to hex string.
    std::string regularPointerToObjectString = ss1.str();

    /* Many other operations can happen here
     * e.g. like passing this string to all sorts of objects which could
     desire to use the instance of CppObject.
     */

     // Convert string to back pointer.
    std::stringstream ss2;
    ss2 << regularPointerToObjectString;
    intptr_t raw_ptr = 0;
    ss2 >> std::hex >> raw_ptr; //Hex string to int.
    void* regularPointerToObjectFromString = reinterpret_cast<void*>(raw_ptr);

    // Convert the void pointer to a CppObject pointer.
    CppObjectStuff::CppObject* pointerToCppObject = static_cast<CppObjectStuff::CppObject*>(regularPointerToObjectFromString);

    // Do something with the CppObject behind the pointer.
    int result = pointerToCppObject->GiveIntValue();

    // Print result.
    std::cout << "This code runs fine:" << "\n";
    std::cout << result;

    // Delete the shared_ptr which in turn deletes the CppObject (something the managed C# code would normally do).
    delete regularPointerToSmartPointer;
}

For reference, CppObject.h:

namespace CppObjectStuff
{
    class CppObject
    {
    public:
        CppObject(int value) { _intValue = value; };
        int GiveIntValue() { return _intValue; };

    private:
        int _intValue;
    };
}

One item that could cause discussion:

I already researched specifically the following tricky case which I concluded should be allowed, see e.g. this bog post. Note, this is similar but not fully identical to the usage in my code above so there could still be stuff I misunderstand:

// Start scope
{
    // Create object in shared_ptr
    std::shared_ptr<void> regularPointerToSmartPointer = std::make_shared<CppObjectStuff::CppObject>(someValue);
}
// Scope ended, shared_ptr went out of scope and should delete the `ppObjectStuff::CppObject` created before.
Stefan
  • 919
  • 2
  • 13
  • 24
  • 2
    _`std::shared_ptr* regularPointerToSmartPointer = new std::shared_ptr();`_ That's a very weird construct. Why should one use such thing? – πάντα ῥεῖ Dec 08 '21 at 11:14
  • A pointer to a smart pointer doesn't make much sense. C++ doesn't need `new` to create objects. What is the specific problem defining `regularPointerToSmartPointer` as a pointer supposed to solve? – Some programmer dude Dec 08 '21 at 11:15
  • 2
    @πάντα ῥεῖ , That is a discussion I want to avoid. The actual situation is quite complicated with lots of legacy code, C++/CLI, external libraries with special build requirements which cannot be reference by all project. I have not all of this in my scope so the best I can do is learn so I can focus on the correct parts or ask for mandate to fix these things. This is just a minimal-working-example showing the operations and I'm wondering if they are fully defined (so not how one can obtain the same result with much nicer code). – Stefan Dec 08 '21 at 11:18
  • @πάντα ῥεῖ , but to answer your question. I do this to simulate something the C++/CLI wrapper does to let the managed C# code manage the lifetime of the `shared_ptr`. I would want things to be different but this is the situation in the code I got. I know it is strange, but if it the behavior of that line of code is fully defined, then I know enough regarding that line of code (for now) :). – Stefan Dec 08 '21 at 11:22
  • 2
    @Stefan if you need to work with kind of legacy API pointers, you might wrap these into smart pointers providing the creation and deletion mechanisms as necessary. I don't really see the point of having a raw pointer to a smart pointer class. – πάντα ῥεῖ Dec 08 '21 at 11:24
  • 1
    Seems to be safe? https://stackoverflow.com/questions/55716295/pointers-to-the-same-address-are-different-from-each-other – user202729 Dec 08 '21 at 11:31
  • @@πάντα ῥεῖ , Thank you, I will look at it. Only my goal with this post is just that I hope to learn if these pointer operations have defined behavior or not. Just to improve my understanding in edge cases. This code shows the operations I'm curious about, which is my goal. Functionally this code is not meant to makes no sense because the entire program (which is very large) is completely cut out and I glued some parts to get a working minimum-working-example. – Stefan Dec 08 '21 at 11:34
  • 2
    Looks like you'd better use intptr_t, see [C++: Is it safe to cast pointer to int and later back to pointer again? - Stack Overflow](https://stackoverflow.com/questions/3567905/c-is-it-safe-to-cast-pointer-to-int-and-later-back-to-pointer-again) – user202729 Dec 08 '21 at 11:35
  • Why all this trickery with sstreams back and forth+reintepret casts instead of using memcpy? – alagner Dec 08 '21 at 12:09
  • 1
    If you work with C++/CLI and managed code, why not go managed all the way and only use the C++/CLI managed pointer? Like `auto^ managedPointerToVoidPointer = gcnew void*();` (only guessing about the syntax)? Or `auto^ managedPointerToSmartPointer = gcnew std::shared_ptr();` (or similar)? – Some programmer dude Dec 08 '21 at 12:16
  • 1
    The ``std::hex`` seems to be misplaced. In ``ss2 << std::hex <> raw_ptr;`` should read ``ss2 >> std::hex >> raw_ptr;`` in my opinion. – Christian Halaszovich Dec 08 '21 at 12:25
  • @user202729 and @Christian Halaszovich , Thank you both. I will use `intptr_t` and use the `std::hex` properly. I edited my code above. – Stefan Dec 08 '21 at 13:26
  • 1
    By the way I think if you put the space between @ and the username it won't work...? – user202729 Dec 08 '21 at 13:54
  • @alagner, Thanks! The actual object in the program is very large, several GB's. Copying it would hurt the performance to much. That is one of the sources why this ugly code came into being, namely, to share this object without reloading / copying / recreating it. But only std::string argument can be passed to consumers of this object. This illustrates already quite some aspects of why this code is as it is. – Stefan Dec 08 '21 at 14:06
  • @Someprogrammerdude , Thanks dude! I will see if the CLI stuff can be improved. I did not make that CLI wrapper stuff but it is using some badly documented CAutoNativePtr class. I tried, what you suggested, having `std::shared_ptr^ test = gcnew std::shared_ptr();` in the wrappers header file. It gives the error: `a handle to a non-manged class is not allowed`. And `gcnew cannot allocate an entity of type std::shared_ptr`. Any thoughts? I'm far from a CLI expert but I think `gcnew` an only allocated non-native types? – Stefan Dec 08 '21 at 14:33
  • 1
    @Stefan either way, I'd replace `void* regularPointerToObjectFromString = reinterpret_cast(raw_ptr);` with `std::memcpy(&regularPointerToObjectFromString, &raw_ptr, sizeof(raw_ptr);` (and a static_assert on sizes of those two matching)`. – alagner Dec 08 '21 at 14:41
  • One more thing: why is the shared_ptr holding void? You could make it explicitly hold CppObject, not that it mattered too much, but seems like it would change nothing functionally, yet improve readability somewhat at least... – alagner Dec 08 '21 at 14:48
  • @alagner, Good idea about the `memcpy` and the `static_assert`. It has been a while since I studied that topic, but wouldn't that create risks regarding Little endian / Big endian (suppose the code would be compiled for a different platform which could actually happen). – Stefan Dec 08 '21 at 15:11
  • 1
    @Stefan not if pointer and integer share the endianess. But even if they don't, I cannot really see how reinterpret_cast could save you here. To me these are the same thing: reinterpreting raw bytes as they go from one type to another, the most notable difference being the fact that memcpy is better defined. – alagner Dec 08 '21 at 15:57
  • @alagner , Thanks and clear, I will propose this change, I agree with you. Also the static_assert to check if the string is could be a complete pointer address, just to be sure, is a good suggestion. – Stefan Dec 08 '21 at 16:47
  • For the rest, is this code simply fully defined? (In my test program it works as expected). – Stefan Dec 08 '21 at 16:48
  • @Stefan one thing that you seem not to be checking, and that is not UB, but may bite you are errors on sstream conversions. Have they even occured? Exceptions can also be harmful (e.g. due to bare new/delete), but I guess those are harder to miss. – alagner Dec 08 '21 at 17:11
  • You introduce 'regularPointerToSmartPointer' in your example, allocate a pointee, but then all you ever do with the raw pointer is dereference it. Your question is about the smart pointer pointed-by the raw pointer. The raw pointer is just noise. This looks *very* much like a mistake lots of people who come to C++ from Java or C# background make. – n. m. could be an AI Dec 13 '21 at 10:09
  • @n.1.8e9-where's-my-sharem. Thanks! Indeed this makes no sense :). Something like this is happening in the code base where I took these operations from. But I think I indeed made a mistake when creating this minimum working example. I will check and update the code (or explain in comments) with respect to what I find. – Stefan Dec 13 '21 at 10:55
  • @n.1.8e9-where's-my-sharem. I double checked. In the actual code, this `regularPointerToSmartPointer` is stored in some class called `CAutoNativePtr`. This class lives in C++-CLI. This class stores a regular C++ pointer as a managed object so it can be handled by C# code. In the C++-CLI code, the `CAutoNativePtr`-object is dereferenced which seems to automatically also dereference the stored C++ pointer. Therefore, because of dereferencing `CAutoNativePtr`-object, we have direct access to the `shared_ptr` object which is then used (by converting to string to pass it to the legacy code). – Stefan Dec 13 '21 at 11:08

0 Answers0