8

I'm using a std::shared_ptr<void> in my application to make a smart pointer which can point to many different types of data structures like structs, vectors, matrices... basically anything. What I'm trying to do is map some names to their data structures.

I'm performing the mapping using a hashtable:

std::unordered_map<std::string, std::shared_ptr<void>>

Can I cast the std::shared_ptr<void> returned by find() back to std::shared_ptr<my_type>? If so, how?

More importantly, is this good practice? Will this increase the complexity too much as the application scales? Or, is there some other completely different, elegant approach?

EDIT

Probably cannot use `Boost.Any' since it uses RTTI.

Also cannot use a base class for all these data structures, since some of them are STL containers like std::vector.

About the shared_ptr delete issue discussed on an answer below, I read that shared_ptr performs type erasure and does store type information to know which destructor to call.

Shared void pointers. Why does this work?

Why do std::shared_ptr<void> work

Why is shared_ptr<void> not specialized?

But I'm not sure about this.

Community
  • 1
  • 1
Bruce
  • 945
  • 3
  • 12
  • 24

4 Answers4

4

This is not good practice. If you don't store additional type information next to your std::shared_ptr (which you can cast using static_pointer_cast) you have undefined behaviour all around. Maybe Boost.Any is an option for you?

If you want to stick with std::shared_ptr<void> please remember to provide a custom deleter function (see make shared_ptr not use delete).

Community
  • 1
  • 1
filmor
  • 30,840
  • 6
  • 50
  • 48
  • 3
    `std::shared_ptr` doesn't invoke UB actually. – Rapptz Mar 26 '14 at 08:17
  • 2
    Not by itself, but casting the pointer to another type (i.e. `p` points to a `Base` object but is casted to `Derived`) and then dereferencing does. – filmor Mar 26 '14 at 08:20
  • @filmor Thanks, can you please explain how UB is introduced? Also, I feel that the fact that I have to consider Boost.Any suggests that my design is flawed fundamentally. If I'm not wrong, Boost.Any will introduce dynamic type-based behavior, which is really against the point of C++ and it's blinding speed. – Bruce Mar 26 '14 at 08:48
  • @filmor I'm actually looking at some C code right now which implements this using `void*` pointers. But I really don't want to use raw pointers in C++! – Bruce Mar 26 '14 at 08:52
  • Using `std::shared_ptr` just for the sake of it doesn't help here. `std::shared_ptr` only makes sense when you have proper deletion behaviour (otherwise what to reference-count for?). You have to store the type information somewhere to be able to dereference the pointer. Whether that is inside of Boost.Any or in a variable you have lying around somewhere doesn't really matter, however, I'd expect Boost.Any is implemented more efficiently :) – filmor Mar 26 '14 at 08:55
  • Could you make your use case more precise? – filmor Mar 26 '14 at 08:56
  • @Bruce the whole point of smart pointers is to control object lifetime. If you've removed their ability to do that, there's really no benefit over a dumb pointer. The dumb pointer will be simpler and easier to reason about. – Mark Ransom Feb 11 '16 at 16:27
3

If you store void*, then at every use point you need to know the exact type you put in, as a cast-to-void*-and-back is only valid if you go to/from the exact same type (not derived-to-void-to-base, for example).

Given that every use point will require knowing the type of the stored pointer, why round trip to void at all?

Have one map per type you store. This has modest overhead over one map per application (O(number of types) memory at run time, similar compile time).

With C++ templates there will be little code duplication.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I think I underestimated the idea of having multiple maps. Thanks a lot, I'll give this a try. – Bruce Mar 26 '14 at 12:53
  • You could eliminate the need to check all the different maps by having one more map to tell you which type the object is, separate from its storage. – Mark Ransom Feb 11 '16 at 17:04
2

Can I cast the std::shared_ptr returned by find() back to std::shared_ptr? If so, how?

Yes. Use pointer casts. That said, in this case, the fact that you need to convert from void* to concrete types is most probably a symptom of poor design.

You should (almost) never need to convert from void* to strongly-typed pointer types ("almost" means I think you should never do it, but there may be some situations I have not considered).

More importantly, is this good practice?

No. As a rule of thumb, you should only convert to void* when you're interested in the value of the memory address, and not interested at all in the stored data.

Will this increase the complexity too much as the application scales?

The first problem I can think of, is that your types are no longer defined in a single place. This means, if you have pointer casts all over the place, when you decide that your class name changed, (or that you no longer use a std::string for a path but a boost::path object, and so on), you will have to go all over the code and update all the types in all the casts you added.

The second problem is, as the application scales, it will carry this casting problem around and propagate it in the code base, and make the situation worse for the entire code base. Your application code will become slightly (or not so slightly) less maintainable due to this. If you have other design compromises throughout the code, past a certain size, your application will become difficult/prohibitive to maintain.

Such an approach will simply make it impossible for you to write loosely-coupled code.

Or, is there some other completely different, elegant approach?

Go over your code and make a list of operations you use on the pointers. Then, add those operations as pure virtual functions in a base class. Implement a concrete template specialization of this base class, in terms of by the stored value type.

Code:

class pointer_holder { // get a better name
public:
    virtual void teleport_pointer() = 0; // example operation
    virtual ~pointer_holder() = 0;
};

template<typename T>
class type_holder: public pointer_holder {
public:
    // TODO: add constructor and such
    virtual void teleport_pointer() {
        // implement teleport_pointer for T
    }
    virtual ~type_holder();
private:
    T value_;
};

Client code:

std::unordered_map<std::string, std::shared_ptr<pointer_holder>> your_map;
your_map["int"] = new type_holder<int>{10};
your_map["string"] = new type_holder<std::string>{"10"};

your_map["int"]->teleport_pointer(); // means something different for each
                                     // pointer type
utnapistim
  • 26,809
  • 3
  • 46
  • 82
  • What if the data structures don't have the same set of operations? – Bruce Mar 26 '14 at 12:32
  • Then implement those operations in the base class and make them throw exceptions (`throw std::runtime_error("teleport_pointer not valid for type");`). This way, if those operations get invoked for those types, you will know about it. – utnapistim Mar 26 '14 at 12:34
2

You can.

#include <memory>
#include <iostream>
#include <string>

using namespace std;

class wild_ptr {
    shared_ptr<void> v;
public:
    template <typename T>
    void set(T v) {
        this->v = make_shared<T>(v);
    }

    template <typename T>
    T & ref() {
        return (*(T*)v.get());
    }
};

int main(int argc, char* argv[])
{
    shared_ptr<void> a;
    a = make_shared<int>(3);
    cout << (*(int*)a.get()) << '\n';
    cout << *static_pointer_cast<int>(a) << '\n'; // same as above

    wild_ptr b;
    cout << sizeof(b) << '\n';
    b.set(3);
    cout << b.ref<int>() << '\n';
    b.ref<int>() = 4;
    cout << b.ref<int>() << '\n';

    b.set("foo");
    cout << b.ref<char *>() << '\n';

    b.set(string("bar"));
    cout << b.ref<string>() << '\n';
    return 0;
}

Output:

3
3
8
3
4
foo
bar
KIM Taegyoon
  • 1,917
  • 21
  • 18