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".

14
  • 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. Commented Jan 28, 2016 at 14:10
  • 13
    "[T]he determined Real Programmer can write FORTRAN programs in any language." - Ed Post, 1983 Commented Jan 28, 2016 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. Commented Jan 28, 2016 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. Commented Jan 28, 2016 at 19:28
  • 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. Commented Jan 28, 2016 at 22:32

5 Answers 5

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

3
  • 11
    Always with the caveat "where appropriate". OOP is not a silver-bullet. Commented Jan 28, 2016 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++? Commented Jan 29, 2016 at 1:15
  • @ChrisCirefice There are no namespaces in C, that's why many library authors use prefixes for their functions. Commented Jan 29, 2016 at 8:47
2

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.

7
  • 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. Commented Jan 29, 2016 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 ). Commented Feb 2, 2016 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. Commented Feb 4, 2016 at 8:43
  • As mentioned in my answer, that also works in clang. Commented Feb 4, 2016 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... Commented Feb 4, 2016 at 15:41
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.

1
  • 4
    Not that I can disagree, but your opinion really should be supported by some elaboration. Commented Jan 29, 2016 at 0:04
-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.

10
  • 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? Commented Jan 28, 2016 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.) Commented Jan 29, 2016 at 0: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. Commented Jan 29, 2016 at 0: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". Commented Jan 29, 2016 at 0:18
  • Which modern C compiler fails that example? Any interesting options needed? Commented Jan 29, 2016 at 0:20
-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 { ... }; 
1
  • 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. Commented Jan 29, 2016 at 0:02

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.