1

Recently I tried to implement my own version of a smart pointer. The implementation looks a bit like the following:

class Var {
private:
    void* value;
    unsigned short* uses;
public:
    Var() : value(nullptr), uses(new unsigned short(1)) { }
    template<typename K>
    Var(K value) : value((void*)new K(value)), uses(new unsigned short(1)) { }
    Var(const Var &obj) {
        value = obj.value;
        (*(uses = obj.uses))++;
    }
    ~Var() {
        if (value == nullptr && uses == nullptr) return;
        if (((*uses) -= 1) <= 0) {
            delete value;
            delete uses;
            value = uses = nullptr;
        }
    }
    Var& operator=(const Var& obj) {
        if (this != &obj) {
            this->~Var();
            value = obj.value;
            (*(uses = obj.uses))++;
        }
        return *this;
    }
};

The implementation should be straight forward, as value holds the pointer and uses counts the references.
Please note the pointer is stored as a void* and the pointer class is not fixed to certain (generic) type.

The Problem

Most of the time the smart pointer does it's job... the exception being the following:

class C {
public:
    Var var;
    C(Var var) : var(var) {}
};
void test() {
    std::string string = std::string("Heyo");
    Var var1 = Var(string);
    C c = C(var1);
    Var var2 = Var(c);
}
void main() {
    test();
}

When running that code the very first instance, var1, does not get deleted after test has run.
Yes, using a void* is not exactly the finest of methods. Yet lets not get off topic. The code compiles perfectly fine (if one might question my use of sub-assign operator). And if the error would be in the deletion of a void* the reference counter, uses, would be deleted but it is not.
I have checked with the destructors before and they all get called as they should.
Do also note that the programm runs without errors.

Thank You all in advance,
Sheldon

Community
  • 1
  • 1
Sheldon
  • 117
  • 1
  • 1
  • 9
  • Calling it a "smart pointer" doesn't say much. What are objects of this class supposed to do? – Pete Becker Aug 16 '17 at 22:08
  • 1
    There's surely something in Boost that does what you need that doesn't involve doing whatever's going on here. Code like `(*(uses = obj.uses))++` is extremely concerning. – tadman Aug 16 '17 at 22:11
  • 2
    Possible duplicate of [What is The Rule of Three?](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three) – Justin Aug 16 '17 at 22:11
  • its supposed to count the references for the pointer it holds. And when the reference count is 0 (/the pointer is not used anymore) it deletes the pointer it holds. So basically an own version of the std::shared_ptr but tailored to my needs – Sheldon Aug 16 '17 at 22:11
  • 2
    What are your "needs" that the Standard Library version can't accommodate? – tadman Aug 16 '17 at 22:12
  • 2
    This is a problem: `this->~Var();`. After that line, `*this` is destroyed; to treat it as not destroyed is not valid – Justin Aug 16 '17 at 22:13
  • 2
    `delete value` will not call `K`'s destructor. Why not just use `std::shared_ptr` instead? It is already a reference-counted smart pointer that does all of the hard work for you. – Remy Lebeau Aug 16 '17 at 22:15
  • there are never any errors thrown, I am just calling the destructor, not deleting `this` – Sheldon Aug 16 '17 at 22:16
  • I do not want my class bound to a specific type. As in the shared_ptr you always would have to have it bound to the same type: `std::shared_ptr`. Mine should be rather open – Sheldon Aug 16 '17 at 22:16
  • 1
    @Sheldon And how are you going to figure out what kind of item is really stored inside of your pointer? How did you determine that something wasn't destroyed? The test code does not perform such checks. – user7860670 Aug 16 '17 at 22:18
  • @Remy Lebeau I tested before and found out otherwise, since the class generally works – Sheldon Aug 16 '17 at 22:19
  • 2
    I'm not sure what `delete value` does. I've never seen anyone even try to do that. I don't know if it's undefined behavior, but it is for certain wrong to expect proper behavior when deleting a `void*` pointing to non-trivially destructable type (which `std::string` is). – Benjamin Lindley Aug 16 '17 at 22:19
  • @VTT this is not the whole pointer class, but the only important part for the question. The whole code is a lot bigger. Lets just say the program never tries to read it wrong – Sheldon Aug 16 '17 at 22:20
  • 3
    @Sheldon, Calling `delete` on `void*` is not right. See https://timsong-cpp.github.io/cppwp/n3337/expr.delete#1. – R Sahu Aug 16 '17 at 22:27
  • Yet the problem does not lie in the delete and since I for now have not found a better alternative I bruteforce this idea. The destructor of `C` get called twice. Meaning that concerns of calling `delete` on a void ptr, as unorthodox as it may be, deletes what it should delete and calls the destructor as it should. The destructor of the string does get called as well. No matter if `var2` is in or not. – Sheldon Aug 16 '17 at 22:43
  • 1
    Off topic: Not much point to `value = uses = nullptr;` at the end of a destructor. One more line and variables are toast. – user4581301 Aug 16 '17 at 22:44
  • If the error would truly be in deleting the `void*` the reference counter would be deleted and I would only be left alone with the `value` but I am not. Which means that the reference count is off – Sheldon Aug 16 '17 at 22:44
  • 3
    You probably have multiple bugs, but the thing with `void*` is a showstopper that derails the whole concept. http://ideone.com/t02HsI Note that the destructor did not run for the `void*` – user4581301 Aug 16 '17 at 22:54
  • 4
    @Sheldon: If your code has undefined behavior, it doesn't matter if it appears to work. It's still wrong. – Benjamin Lindley Aug 16 '17 at 22:55

1 Answers1

4

Three big problems I see with your code are:

  1. you are storing the allocated object pointer as a void*, and then calling delete on it as-is. That will not call the object's destructor. You must type-cast the void* back to the original type before calling delete, but you can't do since you have lost the type info after the Var constructor exits.

  2. you have separated the object pointer and the reference counter from each other. They should be kept together at all times. Best way to do that is to store them in a struct, and then allocate and pass that around as needed.

  3. your operator= is calling this->~Var(), which is completely wrong. Once you do that, the object pointed to by this is no longer valid! You need to keep the instance alive, so simply decrement its current reference counter, freeing its stored object if needed, and then copy the pointers from the source Var and increment that reference counter.

Try this alternate implementation instead (Live Demo):

class Var
{
private:
    struct controlBlockBase
    {
        unsigned short uses;    

        controlBlockBase() : uses(1) { }
        virtual ~controlBlockBase() { }
    };

    template <class K>
    struct controlBlockImpl : controlBlockBase
    {
        K value;
        controlBlockImpl(const K &val) : controlBlockBase(), value(val) {}
    };

    controlBlockBase *cb;

public:
    Var() : cb(nullptr) { }

    template<typename K>
    Var(const K &value) : cb(new controlBlockImpl<K>(value)) { }

    Var(const Var &obj) : cb(obj.cb) {
        if (cb) {
            ++(cb->uses);
        }
    }

    Var(Var &&obj) : cb(nullptr) {
        obj.swap(*this);
    }

    ~Var() {
        if ((cb) && ((cb->uses -= 1) <= 0)) {
            delete cb;
            cb = nullptr;
        }
    }

    Var& operator=(const Var& obj) {
        if (this != &obj) {
            Var(obj).swap(*this);
        }
        return *this;
    }

    Var& operator=(Var &&obj) {
        obj.swap(*this);
        return *this;
    }

    /* or, the two above operator= codes can be
    merged into a single implementation, where
    the input parameter is passed by non-const
    value and the compiler decides whether to use
    copy or move semantics as needed:

    Var& operator=(Var obj) {
        obj.swap(*this);
        return *this;
    }    
    */

    void swap(Var &other)
    {
        std::swap(cb, other.cb);
    }

    unsigned short getUses() const {
        return (cb) ? cb->uses : 0;
    }

    template<class K>
    K* getAs() {
        if (!cb) return nullptr;
        return &(dynamic_cast<controlBlockImpl<K>&>(*cb).value);
    }
};

void swap(Var &v1, Var v2) {
    v1.swap(v2);
}

Update: That being said, what Var is doing is basically the same effect as using a std::any wrapped in a std::shared_ptr, so you may as well just use those instead (std::any is in C++17 and higher only, use boost::any for earlier versions):

class Var
{
private:
    std::shared_ptr<std::any> ptr;

public:
    template<typename K>
    Var(const K &value) : ptr(std::make_shared<std::any>(value)) { }

    void swap(Var &other) {
        std::swap(ptr, other.ptr);
    }

    long getUses() const {
        return ptr.use_count();
    }

    template<class K>
    K* getAs() {
        return any_cast<K>(ptr.get());
    }
};

void swap(Var &v1, Var &v2) {
    v1.swap(v2);
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • @MooingDuck: thanks, I updated my answer and the demo – Remy Lebeau Aug 16 '17 at 23:31
  • @MooingDuck: It is legal to override `std::swap`. – Remy Lebeau Aug 16 '17 at 23:34
  • https://stackoverflow.com/questions/14402990/should-you-overload-swap-in-the-std-namespace "The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified. " (std::swap is not otherwise specified). You should use ADL instead, according to the C++ committee https://stackoverflow.com/questions/11562/how-to-overload-stdswap/2684544#2684544 – Mooing Duck Aug 16 '17 at 23:36
  • @MooingDuck: fine. I have updated my answer again. I can't edit the live demo anymore, the code is really taxing ideone's scripts to the point where they lock up the browser. – Remy Lebeau Aug 17 '17 at 00:09
  • Thank you very much. The code seems to work perfectly after a quick test. Please note: the destructors actually get called as they should without casting back to the original type and this is of course valid after calling `this->~Var();` since I am not deleting `this` rather than just calling a member function. *Yes*, calling the destructor is not a good thing to do as it will invoke the destructors of membervariables. But I have pointers so there is no other destructor called or variable deleted, so I wrote it that way in the compressed version of the code.Rest asured not in the actual – Sheldon Aug 17 '17 at 08:18
  • using `dynamic_cast` led to an error when casting from a derived class to the base class. Its fixed when using a `static_cast` – Sheldon Aug 17 '17 at 11:38
  • 1
    @Sheldon: calling a destructor directly is undefined behavior if the pointer was not allocated with `placement-new`. This code is designed to avoid the casting issue when calling destructors. And please don't use `static_cast`, that will introduce new bugs if you cast to the wrong type that is not actually stored. The whole point of using `dynamic_cast` is to ensure a valid cast to the proper type. If you store an `X` value, you can only retrieve an `X` pointer. If you want a base class pointer, assign the retrieved pointer to a base class pointer after retrieving it. – Remy Lebeau Aug 17 '17 at 13:36
  • I do indeed store though what type to read from the Var instance. But, since I only store the base class directly, a `dynamic_cast` would have to be to the base class. I used a `static_cast` and it works perfectly well with the established code for I check if the base class fits before casting. How could I cast `controlBlockImpl` to `controlBlockImpl` using a dynamic cast? Since `Derived` would only be derrived from `Base` but not `controlBlockImpl` from `controlBlockImpl` I would see an issue there. – Sheldon Aug 17 '17 at 14:49
  • 1
    @Sheldon: you can't cast between `controlBlockImpl` and `controlBlockImpl`, they are unrelated types. If `K` is `Base` (why? You would [slice the stored object](https://stackoverflow.com/questions/274626/)!), you have to retrieve a `Base*` from `controlBlockImpl`, then cast the `Base*` to `Derived*` (which would be invalid due to slicing!). If you have a `Derived` source object, you have to store a `Derived` object via `controlBlockImpl` to avoid slicing, then retrieve a `Derived*` from it, and assign the `Derived*` to a `Base*` without casting. – Remy Lebeau Aug 17 '17 at 15:05
  • 1
    @Sheldon your requirement to store a *copy* of an object inside of `Var` goes against what a smart pointer is intended for. You could just use [`std::variant`](http://en.cppreference.com/w/cpp/utility/variant) or [`std::any`](http://en.cppreference.com/w/cpp/utility/any) instead, wrapped in a [`std::shared_ptr`](http://en.cppreference.com/w/cpp/memory/shared_ptr) for reference counting, to get the same effect that `Var` is trying to accomplish manually. – Remy Lebeau Aug 17 '17 at 15:08
  • What I am doing right now: 1. Get the value as base class (slicing is perfectly fine); 2. get ID from base class (membervariable of base class). 3. From the ID I know what the derived class is. 4. Retrieve the value again but as derived class. It is not possible for me to find the derived class before getting as base. Unless I would try `dynamic_cast` it for any derived type till it is castable to find out the stored type. – Sheldon Aug 17 '17 at 15:13
  • Thank you, I will have a look into them – Sheldon Aug 17 '17 at 15:13
  • 1
    @Sheldon: what you describe will not work. If you store a *copy* of an object, a `Derived` object MUST be stored as `Derived` in order to get back a valid `Derived`. If you store it as a *sliced* `Base`, you CANNOT get a valid `Derived` back! No amount of casting will make that right. Slicing DESTROYS the `Derived` data, leaving ONLY the `Base` data. If you want to store a `Derived` object as a `Base` so you can retrieve the `Derived` later, you CANNOT store a *copy* of the object, you MUST store *a `Base*` pointer to the original `Derived` object*. Which is what all standard smart pointers do – Remy Lebeau Aug 17 '17 at 16:36
  • it does work and that is because what you just described is not what I described. I am storing as the derived – Sheldon Aug 17 '17 at 19:14
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152193/discussion-between-remy-lebeau-and-sheldon). – Remy Lebeau Aug 17 '17 at 19:19
  • @Sheldon: your test case does not even compile: "*error: invalid initialization of non-const reference of type ‘Var&’ from an rvalue of type ‘Var’*" You are passing an rvalue to `get()`, which does not accept an rvalue as input. On a side note, there is a small typo in my answer. For the standalone 2-param `swap()`, the `v2` parameter needs to be a `Var&` reference. I fixed that, but it doesn't apply to this example. – Remy Lebeau Sep 05 '17 at 16:20