使用std::shared_ptr<void>指向任何对象

8
我在我的应用程序中使用了一个 std::shared_ptr<void> 来创建一个智能指针,它可以指向许多不同类型的数据结构,如结构体、向量、矩阵等等。我试图做的是将一些名称映射到它们的数据结构。
我正在使用哈希表执行映射: std::unordered_map<std::string, std::shared_ptr<void>> 我能否将 find()返回的 std::shared_ptr<void> 强制转换回 std::shared_ptr<my_type>?如果可以,如何实现?
更重要的是,这是一个好的实践吗?随着应用程序规模的增长,这会增加复杂性吗?或者,还有完全不同的优雅方法吗?
编辑:
可能无法使用`Boost.Any',因为它使用了RTTI。
也不能为所有这些数据结构使用一个基类,因为其中一些是STL容器,比如 std::vector
关于下面的一个回答中讨论的shared_ptr删除问题,我了解到shared_ptr执行类型擦除并存储类型信息以知道调用哪个析构函数。

共享void指针。为什么这样可以?

为什么std::shared_ptr<void>有效?

为什么没有专门的shared_ptr<void>?

但我对此不是很确定。


1
你可能想看一下Boost Any - Some programmer dude
2
http://static.squarespace.com/static/518f5d62e4b075248d6a3f90/t/5299b27ee4b06b094788820c/1385804424259/voidstar.jpg - Mark Garcia
@JoachimPileborg 谢谢您的建议,您可以看一下我在下面一个答案中的评论吗? - Bruce
为什么RTTI是一个问题?你如何知道存储在map["foo"]中的类型? - David Rodríguez - dribeas
@DavidRodríguez-dribeas 在这个应用程序中,我确实知道映射到任何 foo 的类型。 - Bruce
你是否考虑过使用诸如 boost::variant 这样的变体类型来实现多态,而不是使用 voidboost::variant 是安全且相当高效的,而且你可以轻松地恢复类型信息。 - Chris Beck
4个回答

4
这不是一个好的实践方式。如果你没有在你的std::shared_ptr旁边存储附加类型信息(你可以使用static_pointer_cast进行转换),那么你将在各个方面产生未定义的行为。也许Boost.Any是一个选项?
如果您想坚持使用std::shared_ptr<void>请记住提供自定义删除器函数(请参见make shared_ptr not use delete)。

3
std::shared_ptr<void> 实际上不会导致未定义行为。 - Rapptz
2
不是指针本身,而是将指针转换为另一种类型(即p指向一个Base对象,但被转换为Derived),然后取消引用。 - filmor
@filmor 谢谢,你能解释一下UB是如何产生的吗?而且,我有点觉得我必须考虑Boost.Any这个事实意味着我的设计从根本上存在缺陷。如果我没错的话,Boost.Any会引入动态类型行为,这与C++及其盲目的速度背道而驰。 - Bruce
@filmor 我现在正在查看一些使用void*指针实现此功能的C代码。但我真的不想在C++中使用裸指针! - Bruce
仅仅为了使用std::shared_ptr而使用它在这里并没有帮助。只有当您具有适当的删除行为时,std::shared_ptr才有意义(否则要引用计数什么?)。您必须在某个地方存储类型信息以便能够取消引用指针。无论是在Boost.Any内部还是在某个变量中都没有关系,但是我期望Boost.Any实现更有效 :) - filmor
显示剩余2条评论

3
如果您存储了void*,则每次使用时都需要知道您放入的确切类型,因为将其转换为void*并返回只有在返回到/从完全相同的类型(例如不是派生-到-void-到-base)时才有效。
考虑到每个使用点都需要知道存储指针的类型,为什么要全程在空指针上进行转换?
每种类型一个映射。这对于每个应用程序一个映射而言具有较小的开销(运行时O(类型数量)内存,类似的编译时间)。
使用C++的template可以减少代码重复。

我觉得我低估了拥有多个地图的想法。非常感谢,我会尝试一下的。 - Bruce
你可以通过添加一个更多的映射来消除检查所有不同映射的需要,该映射告诉你对象的类型,而不是它的存储方式。 - Mark Ransom

2
你可以。
#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;
}

输出:

3
3
8
3
4
foo
bar

2
可以。使用指针转换。但是,在这种情况下,你需要从void*转换为具体类型可能是设计不良的症状。
你(几乎)永远不应该需要将void*转换为强类型指针类型(“几乎”意味着我认为你永远不应该这样做,但可能有一些情况我没有考虑)。
不好。作为一个经验法则,只有在你对内存地址的值感兴趣而完全不关心存储的数据时,才应该转换为void*。
这会增加应用程序规模的复杂性吗?
我能想到的第一个问题是,您的类型不再在单个位置定义。这意味着,如果您在各个地方都有指针转换,当您决定更改类名(或不再使用std :: string作为路径而使用boost :: path对象等)时,您将不得不遍历整个代码并更新所有转换中添加的所有类型。
第二个问题是,随着应用程序的扩展,它将携带这个转换问题并在代码库中传播,使整个代码库的情况变得更糟。由于这个原因,您的应用程序代码将变得稍微(或非常)不易维护。如果您在代码中有其他设计妥协,在某个规模以上,您的应用程序将变得难以/禁止维护。
这种方法将使您无法编写松散耦合的代码。
“或者,还有完全不同的优雅方法吗?”
检查您的代码并列出您在指针上使用的操作。然后,在基类中将这些操作添加为纯虚函数。通过存储的值类型实现此基类的具体模板专业化。
代码:
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_;
};

客户端代码:
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

如果数据结构没有相同的操作集怎么办? - Bruce
然后在基类中实现这些操作并使它们抛出异常(throw std::runtime_error("teleport_pointer not valid for type");)。这样,如果这些类型调用了这些操作,您就会知道。 - utnapistim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接