15

I always seem to write code in C that is mostly object oriented, so say I had a source file or something I would create a struct then pass the pointer to this struct to functions (methods) owned by this structure:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Is this bad practice? How do I learn to write C the "proper way".

mosmo
  • 177
  • 1
  • 4
  • 10
    Much of Linux (the kernel) is written this way, in fact it even emulates even more OO-like concepts like virtual method dispatch. I consider that pretty proper. – Kilian Foth Jan 28 '16 at 14:10
  • @KilianFoth Glad to hear, I've been programming C for a while now and I thought I was "doing it wrong" – mosmo Jan 28 '16 at 14:22
  • 13
    "[T]he determined Real Programmer can write FORTRAN programs in any language." - Ed Post, 1983 – Ross Patterson Jan 28 '16 at 15:02
  • 4
    Is there any reason why you don't want to switch to C++? You don't have to use the parts of it that you don't like. – svick Jan 28 '16 at 18:38
  • 5
    This really begs the question of, "What is 'object oriented'?" I wouldn't call this object oriented. I would say it's procedural. (You have no inheritance, no polymorphism, no encapsulation/ability to hide state, and are probably missing other hallmarks of OO that aren't coming off the top of my head.) Whether it's good or bad practice is not dependent on those semantics, though. – jpmc26 Jan 28 '16 at 19:28
  • @jpmc26: You are right with your question about object orientation. Hiding state is possible if you use opaque types, disabling access of functions from other "classes" can be avoided by using static functions and one file per type, interitance is also possible (see chapter 4 of https://www.cs.rit.edu/~ats/books/ooc.pdf). – Residuum Jan 28 '16 at 21:36
  • @svick I'm not sure, I write C++ every now and then and I can write C++, I just prefer not to it's too complicated I can't seem to find time to learn its ins and outs, though I have been looking at strousups core cpp guidelines on GitHub which are interesting. Though nowadays when I program I use something like D, or Go. – mosmo Jan 28 '16 at 21:48
  • 3
    @jpmc26: If you are a linguistic prescriptivist, you should listen to Alan Kay, he invented the term, he gets to say what it means, and he says OOP is all about Messaging. If you are a linguistic descriptivist, you would survey the usage of the term in the software development community. Cook did exactly that, he analyzed the features of languages that either claim to or are considered to be OO, and he found that they have one thing in common: Messaging. – Jörg W Mittag Jan 28 '16 at 22:32
  • @jpmc26: "no polymorphism" you can use function pointers in structs, "no encapsulation/ability to hide state": use opaque data types (https://en.wikipedia.org/wiki/Opaque_data_type). – Giorgio Jan 29 '16 at 17:24
  • @Giorgio The style demonstrated in the question defines functions separately from the struct, so that doesn't seem to be a viable option here. I'll give you encapsulation, though. Although, as I stated above, the issue of whether it's OO or not isn't really relevant to whether it's a good idea. – jpmc26 Jan 29 '16 at 17:51
  • @JörgWMittag This really just shifts the question: "What is 'messaging'?" I really don't see anything in the question I would call "messaging." There's not multiple entities passing messages between one another. The structure here suggests some external sequence of code calling each function in turn. If this is considered object oriented, then I think you would have a hard time distinguishing the object oriented paradigm from procedural or functional paradigms. To be fair, maybe that wasn't a distinction Kay ever intended, but then it makes even less sense to label things with it. – jpmc26 Jan 29 '16 at 20:16
  • @jpmc26: In modern speak, that's virtual method dispatch or vtable dispatch. However, I dislike those terms, because they focus on the implementation mechanism, not the metaphor. To me, the constituent parts are a) polymorphism, which is b) ad-hoc and c) happens at runtime. That's also what Cook identifies. All the other things are either orthogonal (e.g. inheritance) or follow naturally (if all you can do is send messages and observe their responses, data hiding and data abstraction fall out naturally). This means that for example classes in Java are not object-oriented (they describe … – Jörg W Mittag Jan 29 '16 at 21:59
  • Abstract Data Types, not objects, but interfaces are), it also means that lambda-calculus, and by extension functional languages (or rather closures) are object-oriented, at least in the sense that you can implement data abstraction with closures and have a closure with a selector function that closes over some other closures, which all close over some shared closure simulate an object. As to C, I simply don't know enough about it, to judge how "direct" an encoding of OO can be. Do you have to build a full-on Smalltalk interpreter? Can you get by with a couple of void* structs of … – Jörg W Mittag Jan 29 '16 at 22:02
  • … function pointers? I don't know. – Jörg W Mittag Jan 29 '16 at 22:02

5 Answers5

25

No, this is not bad practice, it is even encouraged to do so, although one could even use conventions like struct foo *foo_new(); and void foo_free(struct foo *foo);

Of course, as a comment says, only do this where appropriate. There is no sense in using a constructor for an int.

The prefix foo_ is a convention followed by a lot of libraries, because it guards against clashing with naming of other libraries. Other functions often have the convention to use foo_<function>(struct foo *foo, <parameters>);. This allows your struct foo to be an opaque type.

Have a look at the libcurl documentation for the convention, especially with "subnamespaces", so that calling a function curl_multi_* looks wrong at first sight when the first parameter was returned by curl_easy_init().

There are even more generic approaches, see Object-Oriented Programming With ANSI-C

Residuum
  • 3,312
  • 11
    Always with the caveat "where appropriate". OOP is not a silver-bullet. – Deduplicator Jan 28 '16 at 15:07
  • Doesn't C have namespaces wherein you could declare these functions? Similar to std::string, couldn't you have foo::create? I don't use C. Maybe that's only in C++? – Chris Cirefice Jan 29 '16 at 01:15
  • @ChrisCirefice There are no namespaces in C, that's why many library authors use prefixes for their functions. – Residuum Jan 29 '16 at 08:47
2

It's not bad, it's excellent. Object Oriented Programming is a good thing (unless you get carried away, you can have too much of a good thing). C is not the most suitable language for OOP, but that shouldn't stop you getting the best out of it.

gnasher729
  • 44,814
  • 4
  • 64
  • 126
1

It's not bad. It endorses to use RAII which prevents many bugs ( memory leaks, using uninitialized variables, use after free etc. which can cause security issues ).

So, if you want to compile your code only with GCC or Clang ( and not with MS compiler ), you can use cleanup attribute, that will properly destruct your objects. If you declare your object like that:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Then my_str_destructor(ptr) will be run when ptr goes out of scope. Just keep in mind, that it cannot be used with function arguments.

Also, remember to use my_str_ in your method names, because C does not have namespaces, and it's easy to collide with some other function name.

Marqin
  • 385
  • 2
    Afaik, RAII is about using the implicit calling of destructors for objects in C++ to ensure cleanup, avoiding the need to add explicit resource release calls. So, if I'm not much mistaken, RAII and C are mutually exclusive. – cmaster - reinstate monica Jan 29 '16 at 17:31
  • @cmaster If you #define your typenames to use __attribute__((cleanup(my_str_destructor))) then you will get it as implicit in whole #define scope ( it will be added to all your variable declarations ). – Marqin Feb 02 '16 at 13:35
  • That works if a) you use gcc, b) if you use the type only in function local variables, and c) if you use the type only in a naked version (no pointers to the #define'd type or arrays of it). In short: It's not standard C, and you pay with a lot of inflexibility in use. – cmaster - reinstate monica Feb 04 '16 at 08:43
  • As mentioned in my answer, that also works in clang. – Marqin Feb 04 '16 at 11:44
  • Ah, I didn't notice that. That indeed makes the requirement a) significantly less severe, as that makes __attribute__((cleanup())) pretty much a quasi-standard. However, b) and c) still stand... – cmaster - reinstate monica Feb 04 '16 at 15:41
  • solution for b) is here and for c) you can #define those pointers/arrays – Marqin Feb 04 '16 at 20:50
  • There's also a library that uses this attribute to implement C++ shared_ptr/unique_ptr in C - https://github.com/Snaipe/libcsptr – Marqin Feb 04 '16 at 20:52
-2

There can be many advantages to such code, but unfortunately the C Standard was not written to facilitate it. Compilers have historically offered effective behavioral guarantees beyond what the Standard required that made it possible to write such code much more cleanly than is possible in Standard C, but compilers have lately started revoking such guarantees in the name of optimization.

Most notably, many C compilers have historically guaranteed (by design if not documentation) that if two structure types contain the same initial sequence, a pointer to either type may be used to access members of that common sequence, even if the types are unrelated, and further that for purposes of establishing a common initial sequence all pointers to structures are equivalent. Code which makes use of such behavior can be much cleaner and more type-safe than code which does not, but unfortunately even though the Standard requires that structures sharing a common initial sequence must be laid out the same way, it forbids code from actually using a pointer of one type to access the initial sequence of another.

Consequently, if you want to write object-oriented code in C, you'll have to decide (and should make this decision early on) to either jump through a lot of hoops to abide by C's pointer-type rules and be prepared to have modern compilers generate nonsensical code if one slips up, even if older compilers would have generated code which works as intended, or else document a requirement that the code will only be usable with compilers that are configured to support old-style pointer behavior (e.g. using an "-fno-strict-aliasing") Some people regard "-fno-strict-aliasing" as evil, but I would suggest that it is more helpful to think of "-fno-strict-aliasing" C as being a language which offers greater semantic power for some purposes than "standard" C, but at the expense of optimizations which might be important for some other purposes.

By means of example, on traditional compilers, historical compilers would interpret the following code:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

as performing the following steps in order: increment the first member of *p, complement the lowest bit of the first member of *t, then decrement the first member of *p, and complement the lowest bit of the first member of *t. Modern compilers will rearrange the sequence of operations in a fashion which code which will be more efficient if p and t identify different objects, but will change the behavior if they don't.

This example is of course deliberately contrived, and in practice code which uses a pointer of one type to access members that are part of the common initial sequence of another type will usually work, but unfortunately since there's no way of knowing when such code might fail it's not possible to safely use it at all except by disabling type-based aliasing analysis.

A somewhat less contrived example would occur if one wanted to write a function to do something like swap two pointers to arbitrary types. In the vast majority of "1990s C" compilers, that could be accomplished via:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

In Standard C, however, one would have to use:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

If *p2 is kept in allocated storage, and the temporary pointer isn't kept in allocated storage, the effective type of *p2 will become the type of the temporary pointer, and code which attempts to use *p2 as any type which didn't match the temporary-pointer type will invoke Undefined Behavior. It is to be sure extremely unlikely that a compiler would notice such a thing, but since modern compiler philosophy require that programmers avoid Undefined Behavior at all cost, I can't think of any other safe means of writing the above code without using allocated storage.

supercat
  • 8,445
  • 23
  • 28
  • Downvoters: Care to comment? A key aspect of object-oriented programming is the ability to have multiple types share common aspects, and have a pointer to any such type be usable to access those common aspects. The OP's example doesn't do that, but it's barely scratching the surface of being "object-oriented". Historical C compilers would allow polymorphic code to be written in a much cleaner fashion than is possible in today's standard C. Designing object-oriented code in C thus requires that one determine what exact language one is targeting. With what aspect to people disagree? – supercat Jan 28 '16 at 23:26
  • Hm... mind you show how the guarantees the standard gives don't allow you to cleanly access members of the common initial sub-sequence? Because I think that's what your rant about the evils of daring to optimize within the bounds of contractual behavior hinges on this time? (That's my guess what the two downvoters found objectionable.) – Deduplicator Jan 29 '16 at 00:00
  • OOP does not necessarily require inheritance, so compatibility between two structs isn't much of an issue in practice. I can get real OO by putting function pointers into a struct, and invoking those functions in a particular manner. Of course, this foo_function(foo*, ...) pseudo-OO in C is just a particular API style that happens to look like classes, but it should more properly be called modular programming with abstract data types. – amon Jan 29 '16 at 00:14
  • @Deduplicator: See the indicated example. Field "i1" is a member of the common initial sequence of both structures, but modern compilers will fail if code tries to use a "struct pair*" to access the initial member of a "struct trio". – supercat Jan 29 '16 at 00:18
  • Which modern C compiler fails that example? Any interesting options needed? – Deduplicator Jan 29 '16 at 00:20
  • See http://goo.gl/zV5FQw for an on-line compiler tester with the code pre-loaded. In case you don't understand assembly code, bx lr is the ARM equivalent of "return", so the compiler optimizes the function down to nothing. – supercat Jan 29 '16 at 00:25
  • ;-) Look where I just was: https://goo.gl/WxKrZO -O2 or more seems to break it... – Deduplicator Jan 29 '16 at 00:26
  • I think that's simply a compiler-bug though. Will look at it a bit more tomorrow. – Deduplicator Jan 29 '16 at 00:33
  • @Deduplicator: Under a reasonable reading of the Standard, there would be no way to pass the code above two pointers to the same object without invoking Undefined Behavior, and consequently a compiler would be justified in doing whatever it likes. I think the C89 rules were badly written but the common interpretations of them allowed useful optimizations while allowing programmers to do anything that could be done in their absence. Modern philosophy, however, suggests that compiler writers should feel no obligation to go beyond what a compiler-friendly reading of the standard would require. – supercat Jan 29 '16 at 16:54
  • @Deduplicator: Consider also how one would write a function which, given two pointers to pointers of arbitrary type, will swap them (see above). Having to use malloc to create temporary storage seems absurd, but I don't think the Standard allows any alternative. [Note: rather than treating the pointers as void* the function should treat them as struct DUMMY_TYPE*, since structure pointers are allowed to have a different size from void*; violations of the type rules, however, result in UB even when all pointers are the same size]. – supercat Jan 29 '16 at 17:12
-3

Next step is to hide the struct declaration. You put this in the .h file:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

And then in the .c file:

struct foo_s {
    ...
};
Solomon Slow
  • 1,213
  • 6
    That might be the next step, depending on the aim. Whether it is or not, this doesn't even remotely try to answer the question though. – Deduplicator Jan 29 '16 at 00:02