2

Suppose I'm building a linked list (the real data structure is completely different, but a linked list suffices for the question) whose nodes look like

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

For my data structure, I have a lot of functions with return type struct node *, and I want the user to treat this type as opaque. In the linked list example, such a function could be for example get_next(struct node<T> *n) or insert_after(struct node<T> *x, struct node<T> *y). Only very few functions, namely those that allocate nodes or get/set their data field, need to know anything about T.

Is there a nicer way to "ignore T" and let the user interact only with something like a typedef struct node * opaque_handle for those functions that don't ever have to care about T? My gut reaction, coming from C, is just to cast to and from void*, but that doesn't sound very elegant.

Edit: CygnusX1's comment has convinced me that I'm asking for too many guarantees from the type system at the same time that I'm trying to circumvent too many of those guarantees. I will fall back to letting T be void * at the cost of casting and indirection.

gspr
  • 10,367
  • 3
  • 38
  • 68
  • maybe it doesn't sound elegant but it is widely used practice. read about [pimpl](https://en.wikipedia.org/wiki/Opaque_pointer) for example. Have a forward-declaration `struct T` in public header and use `T*` in `struct node` (doesn't need to be template in this case) – mvidelgauz Jun 25 '16 at 17:45
  • Is there a specific need to design your data structure in a way that exposes the `node` type? Why not model it after the other containers in the standard (user interface functions return `T` instead of `node`)? – user2296177 Jun 25 '16 at 18:02
  • You might want to have a look how [STL](http://www.cplusplus.com/reference/list/list/) collections deal with the problem. They don't actually give end-user access to implementation structure like e.g. `node`. Their interface contains an abstract understandable for end-user types as `iterator` `value_type` `reference` and so on. – W.F. Jun 25 '16 at 18:02
  • @user2296177: My point is *exactly* to avoid exposing `node` (for almost all functions). I want something à la `typedef struct node * handle_t` and have most functions take and return `handle_t`. – gspr Jun 25 '16 at 18:03
  • @gspr The thing is that you don't have to expose `node` at all. From the POV of the user, your data structure functions return `T`'s. Memory management should be done internally by the data structure or by a base class. Like @WF suggested, just look at one of the standard containers for inspiration on interface. – user2296177 Jun 25 '16 at 18:35

2 Answers2

1

While you don't care about what T is, you most like want to differenciate it from a different type - say U, don't you? You probably want the following to raise an error:

node<T>* elem1 = ...
node<U>* elem2 = ...
elem1 = elem2

There are a few ways to make your code simpler without sacrificing the type checking or run-time perforamce:

  • If you use C++11, consider using auto instead of explicitly naming the type when using your functions
  • If node<T> is very common in your code, you can set a global-scope typedef

Also note, that in the context of node<T> definition, using a plain node (without template arguments) is allowed.

If you really want to hide the contents of the node, consider implementing the pimpl pattern as suggested by mvidelgauz.

Community
  • 1
  • 1
CygnusX1
  • 19,236
  • 3
  • 55
  • 100
  • To the first part of your answer: Well, for *some* functions, yes. But for a lot of the functions (such as inserting a new, already-allocated, node in the linked list example) there is absolutely no need to know anything about the fields in the struct following the first one. A lot of my functions *only* need to know that the struct starts with a pointer to a node. `T` can be a million bytes of crazy stuff for all they care. – gspr Jun 25 '16 at 17:57
  • Regarding the `typedef`: But I can't do `typedef struct node * foo`, can I? If I could, almost all of my functions would take and return the `foo` type, and would work by knowing that "`foo` is a pointer to a `node`, and I don't care what `T` is, for I'll only need the first field which is always a pointer to another `node`". – gspr Jun 25 '16 at 18:00
  • @gspr You still need to know that the second element you insert is of the same type as the first - even if it is million bytes of crazy stuff. – CygnusX1 Jun 25 '16 at 18:21
  • Do I, if it's already allocated elsewhere (by someone with knowledge of what `T` is, of course)? – gspr Jun 25 '16 at 18:22
  • @gspr Regarding the global-level typedef - you shouldn't use it for the `node` definition. However, if `node` for a specific `T` is dominant in your project and has some meaning that you can name, you can definetely `typedef` it and use it as such. I would still name it `FooPtr` and not `Foo` if you name the pointer type. – CygnusX1 Jun 25 '16 at 18:22
  • 2
    @gspr You need to know that the stuff allocated by someone else matches the type contained by your list. Consider that you could have `node` and `node` types flying around. You still need to ensure that you don't mistakenly put a `node` into a list of `T`-s. Knowledge that `T` and `U` are different is useful. You could in theory walk around it with type tagging without providing implementation, but having a plain `T` and `U` is usually simpler. – CygnusX1 Jun 25 '16 at 18:25
  • Hmm, yeah, good point. I guess I want to opt out of that specific type-safety. – gspr Jun 25 '16 at 18:27
0

If you can use boost, then boost::any or boost::variant may be able to help implement heterogeneous containers.

Is something like this what you're after?:

#include <iostream>
#include <boost/any.hpp>
#include <list>

using Collection = std::list<boost::any>;
using Node = Collection::iterator;

static Collection anys;

template<typename T>
Node insert_after(T const& obj, Node start_pos = anys.end())
{
    return anys.insert(start_pos, boost::any(obj));
}

void print_type(boost::any const& a)
{
    if (a.type() == typeid(int)) { std::cout << "int" << std::endl; }
    else if (a.type() == typeid(float)) { std::cout << "float" << std::endl; }
}

int main()
{
    const auto node1 = insert_after(int(1));
    const auto node2 = insert_after(float(2.57));
    const auto node3 = insert_after(int(3));

    std::cout << boost::any_cast<int>(*node1) << std::endl;
    std::cout << boost::any_cast<float>(*node2) << std::endl;
    std::cout << boost::any_cast<int>(*node3) << std::endl;

    print_type(*node1);
    print_type(*node2);
    print_type(*node3);

    return 0;
}

Outputs:

1
2.57
3
int
float
int
mkal
  • 140
  • 5
  • He doesn't want a heterogeneous container, he simply wants to obscure the fact that his nodes are `node` with some other type that has no information about `T`. – user2296177 Jun 25 '16 at 18:27
  • Thanks for the answer, but most of boost is so black-magic to me that I try to avoid using it unless absolutely neccessary :-) – gspr Jun 25 '16 at 18:27
  • @user2296177 Ah, I thought based on the other comments that OP wanted to build a list whose nodes may contain different T's, sorry. – mkal Jun 25 '16 at 18:36