使用C++生成泛型类型-一个具有共享实现的模板

Using C++ to make a generic type - a template with shared implementation

本文关键字:一个 共享 实现 C++ 泛型类型 使用      更新时间:2023-10-16

例如,考虑一个简单的数据结构,如链表。在C中,它可能看起来像:

struct Node
{
    struct Node *next;
    void *data;
};
void *getLastItem(struct Node*);
...

我希望有相同的结构和函数,但通过声明data字段的类型来进行更好的类型检查,它将始终是指向某个对象的指针。示例用法:

Node<Thing*> list = getListOfThings();
Thing *t = list->data;
t = getLastItem(list);
...

但我不想像普通模板那样为每种类型的指针生成一个实现。换句话说,我想要一些更像Java、ML和其他语言的泛型或参数类型。我只是尝试了下面的代码作为测试。非类型化的类C部分最终会放在实现文件中,而模板和函数声明则会放在头文件中。我假设它们会被优化掉,剩下的机器代码与C版本大致相同,只是会进行类型检查。

但我对C++不太在行。。。有没有办法改进这一点,或者使用更惯用的C++,也许是模板专业化?

#include <stdio.h>
struct NodeImpl
{
    NodeImpl *next;
    void *data;
};
void *getLastItemImpl(NodeImpl *list)
{
    printf("getLastItem, non-template implementation.n");
    return 0;  // not implemented yet
}
template <typename T>
struct Node
{
    Node<T> *next;
    T data;
};
template <typename T>
T getLastItem(Node<T> *list)
{
    return (T)getLastItemImpl((NodeImpl*)list);
}
struct A { };
struct B { };
int main()
{
    Node<A*> *as = new Node<A*>;
    A *a = getLastItem(as);
    Node<B*> *bs = new Node<B*>;
    B *b = getLastItem(bs);
}

这正是Boost.PointerContainer所做的,请检查其实现。基本上,它所做的是实现void*的专用化,并将参数输入和输出的任何其他实现转发给static_cast

struct Node
{
    struct Node *next;
    void *data;
};
void *getLastItem(struct Node*);
...

这在C中很常见,但在C++中却不常见。在C++中,它通常看起来是这样的:

template<typename T>
struct Node
{
    struct Node *next;
    T data;
};
T& getLastItem(const Node&);
...

注意重要的区别——C版本有另一个间接级别,以便共享实现,而C++版本不需要这样做。这意味着C版本有另一个n动态内存分配,其中n是列表中的项目数。考虑到每次分配通常需要获得一个全局锁,每次分配通常至少有16字节的开销,以及内存管理器给一方带来的所有开销,C++版本的优势并非微不足道,尤其是在考虑缓存位置等因素时。

换句话说,对于Node<int>,C++版本存储int,而C版本存储int *,以及int的动态分配。

当然,在90%的时间里,链表是一个可怕的数据结构。

如果必须使用链表,并且必须对数据成员使用动态分配,那么"用void*s替换指针"的想法也不无道理。但是,如果您可以访问C++11编译器(VS2010、最新的GCC版本等(,则应该使用std::is_pointerstatic_assert来断言您依赖于T作为指针类型,并且应该在接口方法中使用static_cast而不是C样式转换。C风格的强制转换会让人执行Node<SomeTypeBiggerThanVoidPtr>,它会编译,但会在运行时爆炸。

正如其他答案和注释所说,使用std::forward_list或其他现有库。如果你拒绝,这更像我会做的:

#include <stdio.h>
struct NodeImpl
{
    NodeImpl *next;
    void *data;
public:    
    // we have pointers, so fulfill the rule of three
    NodeImpl() : next(NULL), data(NULL) {}
    ~NodeImpl() {}
    NodeImpl& operator=(const NodeImpl& b) {next = b.next; data = b.data; return *this;}
    // This function now a member.  Also, I defined it.
    void* getLastItem()
    {
        if (next)
            return next->getLastItem();
        return data;
    }
    void* getData() {return data;}
    void setData(void* d) {data = d;}
};
// the template _inherits_ from the impl
template <typename T>
struct Node : public NodeImpl
{
    Node<T> operator=(const Node<T>& b) {NodeImpl::operator=(b);}
    // we "redefine" the members, but they're really just wrappers
    T* getLastItem()
    { return static_cast<T*>(NodeImpl::getLastItem());}
    T* getData() {return static_cast<T*>(NodeImpl::getData());}
    void setData(T* d) {NodeImpl::setData(static_cast<void*>(d));}
    //or, if you prefer directness...
    operator T*() {return static_cast<T*>(NodeImpl::getData());}
    Node<T> operator=(T* d) {NodeImpl::setData(static_cast<void*>(d));}  
};

struct A { };
struct B { };
int main()
{
    Node<A> as;  //why were these heap allocated?  The root can be on the stack
    A *a = as.getLastItem();
    Node<B> bs; //also, we want a each node to point to a B, not a B*
    B *b = bs.getLastItem();
    B* newB = new B;
    bs = newB;  //set the data member
    newB = bs;  //read the data member
}

http://ideone.com/xseYk请记住,这个对象并没有真正封装next或数据,所以您必须自己管理所有这些。