1

Let's say I have a container in C, for example something similar to C++' std::deque:

struct deque
{
    // blah
};

struct deque* deque_create(size_t element_size, size_t init_deq_size);
void* deque_get(struct deque const* deq, size_t index);
void* deque_push_back(struct deque* deq, void* elt);
void deque_pop_front(struct deq* deq);
//etc.

All of this works rather nice if the vector stores objects themselves rather than pointers to objects:

int* i = deque_get(deq, 5);

Still some explicit casting and nesting parentheses sometimes seems necessary:

for(size_t i = 0; i < deque_size(deq); i++) {
    (*(int*)deque_get(deq, i))++;
}

I find it ugly to have to write (*(int*)deque_get(deq, i))++; where in C++ I'd be able to just write deq[i]++;.

However, the ugliness seems to go off-charts when this container is used to store pointers to objects.

For example, let's assume we are storing pointers to this struct:

struct foo
{
    int bar;
    double baz;
};

Then getting the bar field of the fifth element of our container would look like this:

(*(struct foo**)deque_get(deq, 5))->bar;

Or like this:

struct foo** foo = deque_get(deq, 5);
(*foo)->bar;

Or like this:

struct foo* foo = *(struct foo**)deque_get(deq, 5);
foo->bar;

(Whereas in C++ we'd only have to write deq[5]->bar).

But nothing beats in ugliness checking if an element is not null and if so, if its bar field is >= 5:

if(*(struct foo**)deque_get(deq, i) != NULL &&
        (*(struct foo**)deque_get(deq, i))->bar >= 5) {
    // do_something
}

In short: When I use containers in C, I tend to write lots of ugly code consisting of lots of casts and parentheses, a problem that can be somewhat mitigated by adding unecessary lines of code - a solution that is pretty inconvenient if the need to resort to it arises frequently.

How to write such code readably and concisely?

12
  • 3
    Have you looked at e.g. GLib and how that handles this? The answer is macros, which are still ugly, but less ugly than explicit casts everywhere. – Philip Kendall Jun 11 '17 at 6:13
  • 11
    Hey, it is C, a language without templates, what do you expect? One main reason why templates were added to C++ was most probably to avoid exactly these kind of problems – Doc Brown Jun 11 '17 at 7:48
  • 5
    @gaazkam: The answer in C is usually either to create type-specific containers, to hide the casts behind macros or to just accept the casts as part of what is needed to use type-generic containers. – Bart van Ingen Schenau Jun 12 '17 at 7:49
  • 4
    The closest you go to the processor, the uglier and less comfortable the language gets. More abstracted languages usually add constructs and syntactic sugar to avoid such ugly code, but since C is really close to machine code some ugliness is unavoidable. Of course, you can use macros, but this is just a different type of ugly. – T. Sar Jun 12 '17 at 11:38
  • 3
    @gaazkam If a code review of C code consists of the reviewer "laughing [and] swearing", then the reviewer should arguably learn more about C. I can come up with any number of constructs that I can write in Java too which I can't write without considerable ugliness in C, but that doesn't mean that C should be held to Java's standards, or for that matter that Java should be held to C's standards. C++ is no different in that regard. Just because they happen to look superifcially similar doesn't make them the same language. – user Jun 12 '17 at 13:16
2

If you want your code to be more readable, then make it more verbose so that the intent is obvious and concrete!

For example, your first loop would be much more readable (at least to some folks) if it was written like this:

for (i = 0; i < deque_size(deq); i++) {
    int *x;

    x = deque_get(deq, i);
    *x++;
}

This is of course a somewhat contrived example and I personally don't find somewhat isolated uses of expressions such as (*(int *) deque_get(deq, i))++ to be too ugly. I wouldn't use a macro though -- they can lead to too many hard-to-find bugs once they get a tiny bit more complicated.

In any case, don't try to put too much into any given expression. Be explicit about your intent by writing it out in full. Any decent optimizing compiler will generate the same code from this as it would from the way you wrote it.

I don't understand why you are storing pointers to pointer to foo (i.e. struct foo **) in your second example though. Don't do that. You never want to manipulate those pointers in the same way you might manipulate an int object, for example, and you would also need somewhere else to store the intermediate pointer to foo pointers. Just store struct foo * and then you can write:

struct foo *x;

x = deque_get(deq, i);
if (x && x->bar >= 5) {
    // do_something
}

Macros can help though if they're well designed and named. Take a look at the queue.h macros available by default in many systems, e.g. all BSD and Linux systems: QUEUE(3)

5
  • I don't understand why you are storing pointers to pointer Because I don't want struct foo to be copied, and otherwise I’d have no way to store it in the queue but to copy it. In C++ I'd specialize the container with a specialization of std::reference_wrapper. In C, I have no choice but to store pointers to this structure in my container. Due to the lack of references in C functions like queue_get must return a pointer to the stored element, so they end up returning a pointer to a pointer to the struct. I honestly don't know what's wrong with this reasoning, could You enlighten me? – gaazkam Jun 12 '17 at 19:34
  • I'm not sure that makes any sense to me. If you store a plain struct foo * then you are storing a pointer to the struct. You cannot store the struct itself (unless it happened to fit in the same space a void * occupies). If you store pointers to pointers to a struct then you'll need somewhere else to store the pointer to the struct (e.g. an array or another list). – Greg A. Woods Jun 12 '17 at 20:08
  • I'm still not sure where the problem lies. I really think I can store the struct itself and it doesn't need to fit in the same space a void* occupies. Sometimes a code is worth a thousand words, so perhaps this will allow me to clarify my intent better. Here is an example simplistic implementation of such a container: ideone.com/Bl3xgd Could you kindly take a look there? – gaazkam Jun 12 '17 at 20:56
  • Ah, yes, thank you for the example! I don't know C++ very well, but I understood std::deque to be a list, and your API in the question suggested a list of void * objects. In your posted code you use a new type of object and API. In vec1, the underlying storage is an "array of objects the size of struct foo" and your vector_get(vec1, n) can be considered to return a struct foo *. But your vec2 stores "array of objects the size of void *", with the pointed to objects having separate storage. You do a number of error-prone things with vec2 (leaks are far more possible). – Greg A. Woods Jun 12 '17 at 21:33
  • 2
    +1 for using temporary variables. It makes a huge difference in clarity when dealing with functions returning void* pointers. Getting element from collection and then modifying it should be treated as 2 different and separate operations. – user694733 Jun 13 '17 at 7:47
0

If you want to work in C with general-purpose data structures, algorithms, etc. then casting just comes with the territory. Most of the time it can be implicit, but it has to be explicit when you aren't using a void* pointer to be passed or assigned to anything, e.g.

With C you're working with a type system so simple that allows you to x-ray it and work at the level of bits and bytes without worrying about copy ctors and dtors and so forth. In C++ functions like memcpy and memset are evil, horrid functions because they bulldoze over the rich features of the C++ type system by operating with bits and bytes in a language not intended to be used so much for such a purpose. In C, these are right-arm functions to often be used on a daily basis.

C revolves around operating on homogeneous collections of bits and bytes. Data types are just a way to simplify working with bits and bytes, specifying what bits and bytes are what in a given context and how they are aligned. In many cases in C, the same bits and bytes could represent one data type at one moment and another the next. It's just slightly above assembly in that regard. In assembly code, there are no data types. In C, there are data types but they're often transient concepts just to aid the programmer in working with bits and bytes and making sure they invoke the appropriate machine instructions on those bits and bytes.

That often means that as you translate to/from raw bits and bytes, you have to specify what type of data you want the bits and bytes to be treated as. That's the price you pay for being able to so easily work at the level of bits and bytes and not data types. If you want to debug such C code, you also have to get used to explicitly specifying what data type you want bits and bytes to be interpreted as in the debugger. It's not convenient unless you're writing code that benefits from working with raw bits and bytes.

I would argue that C++ is a much, much, much higher level language because of just a handful of features it originally added on top of C, and those would be virtual functions, destructors, and constructors. Those 3 features change the entire nature of the language to a point where you can no longer assume that an arbitrary type, T, can merely be treated as a homogeneous collection of bits and bytes to be, say, copied around with memcpy elsewhere or initialized with memset to set its bits to zero. You can't apply "polymorphism" at the bits and bytes level anymore. It now means that you must always write such generic code against the assumption that you will need to call functions specific to T to do these types of things. The idea of a data type is no longer something transient or something that can be ignored in specific contexts, and that alone leads to a completely different philosophy and language suitable for very different purposes, and all because of what it added, not what it changed or removed.

C For Data Structures and Allocators

When you need to write code like your own proprietary data structures and memory allocators, then C becomes more convenient than C++. Try writing a fully standard-compliant deque in C++ with perfect exception-safety, placement new and manual dtor invocation in all appropriate areas, std::allocator support, forward and reverse iteration, both read-only and mutable iterators, range ctor, fill ctor, initializer list support, with ctors having all appropriate overloads for when the user specifies a custom allocator and when they don't, erase, range erase, insert, range insert, push front, push back, two overloads of operator[] (read-only and mutable), two overloads of front back element access, template specializations for PODs which can use POD-related optimizations (ex: no-throw assumptions), etc. It's definitely not easy, especially the exception-safety part. With C you can just focus entirely on the data structure as an arrangement of bits and bytes which can give you so much time to focus on cache-friendly memory layouts and the algorithm itself. It's so much easier even if the end result is harder to use.

In my case a nice balance for me is using C when I need to implement proprietary data structures, when nothing in the C++ standard library suits the purpose (which is actually quite common for me since the C++ standard library doesn't provide very suitable data structures for, say, commercial-quality raytracing or entity-component systems, and you're not going to get a competitive raytracer by just using someone else's). Then for the mid and high-level code which benefits from just using existing data structures, I use C++. And for the highest-level user-end code which needs very rapid iterations and turnarounds, I use embedded Lua scripts and have been exploring the idea of compiling and dynamically linking C and C++ code on the fly while the engine is running.

I fell in love for a time with writing general-purpose data structures in C++ (actually doing all the work mentioned above for full standard-compliance) and thought C++ was the ideal language for designing and implementing very competitive proprietary data structures because of how much rich feature support you get the moment you provide, say, standard-compliant iterators for your container, and the cost-free abstractions with static polymorphism. I fell out of love with it years later and came full circle back to C, which I now believe is the dream language for people who want to implement the most performance-critical data structures and allocators while still being able to get on with their lives and work towards higher-level design aspects of their product. C++ is ideal for using existing data structures and allocators which other people already put a great deal of effort to develop towards full-compliance; you can reap a great deal of benefits provided they spend that enormous time on their data structure. But if you want to get on with your life while still being required to write competitive new implementations of data structures with code that builds in a fraction of a second, then I think C is a much more productive fit but again, it does mean you will have your share of casting here and there (implicit or explicit).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.