使用 C++创建通用类型 - 具有共享实现的模板

发布于 2024-12-05 16:51:58 字数 1236 浏览 2 评论 0原文

作为一个例子,考虑一个简单的数据结构,比如链表。在 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);

}

As an example, consider a simple data structure like a linked list. In C, it might look like:

struct Node
{
    struct Node *next;
    void *data;
};

void *getLastItem(struct Node*);
...

I'd like to have the same struct and functions, but with better type checking by declaring the type of the data field, which will always be a pointer to something. An example use:

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

But I don't want to generate an implementation for every type of pointer, as happens with a normal template. In other words, I want something more like a generic or parametric type from Java, ML, and other languages. I just tried the code below as a test. The untyped C-like part would eventually go in a implementation file, while the template and function declarations would be in the header file. I'm assuming they would be optimized away and I'd be left with machine code that is about the same as the C version, except it would be type-checked.

But I'm not great with C++... Is there a way to improve this, or use more idiomatic C++, perhaps template specialization?

#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);

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

双马尾 2024-12-12 16:51:58

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

This is exactly what Boost.PointerContainer does, check its implementation. Basically what it does is implement the specialization for void*, and have any other implementation forward to it static_casting the parameters in and out.

看海 2024-12-12 16:51:58
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,C++ 版本存储一个 int,而 C 版本存储一个 int * 以及一个int 的动态分配。

当然,这并没有考虑到链表在 90% 的情况下都是一种可怕的数据结构。

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

struct Node
{
    struct Node *next;
    void *data;
};

void *getLastItem(struct Node*);
...

This is common for C, but not for C++. In C++ it usually looks like this:

template<typename T>
struct Node
{
    struct Node *next;
    T data;
};

T& getLastItem(const Node&);
...

Note the important difference -- the C version has another level of indirection in order to share implementations, while the C++ version need not do this. This means the C version has another n dynamic memory allocations, where n is the number of items in the list. Given that each allocation usually requires obtaining a global lock, often has at least 16 bytes of overhead per allocation, as well as all the overhead the memory manager brings to the party, the advantage of the C++ version is not insignificant, particularly when you include things like cache locality in the considerations.

Put another way, for Node<int>, the C++ version stores an int, while the C version stores an int *, along with a dynamic allocation for the int.

This of course discounting that a linked list is a horrendous data structure 90% of the time.

If you must use a linked list, and if you must use dynamic allocation for the data members, then your idea of "replace the pointers with void*s" is not unreasonable. However, if you have access to a C++11 compiler (VS2010, recent GCC versions, etc.), you should put in an assert that you depend on T being a pointer type, using std::is_pointer and static_assert, and you should use static_cast rather than C-style casts in your interface methods. The C style cast would let someone do Node<SomeTypeBiggerThanVoidPtr>, and it would compile, but explode at runtime.

心房的律动 2024-12-12 16:51:58

正如其他答案和评论所说,使用 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 或数据,因此您必须自己管理所有这些。

As the other answers and comments said, use std::forward_list, or another existing library. If you refuse, this is more like I would do:

#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
Keep in mind that this object doesn't encapsulate next or data really, so you have to manage all of that yourself.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文