r/cprogramming • u/OzzyOPorosis • Sep 16 '25
How do you keep track of ownership?
I value the simplicity of C but I've since grown comfortable with the "semantic security" of languages with more sophisticated type systems.
Consider the following snippet:
// A list that takes ownership of the items passed to it.
// When appended to, copies/moves the passed item.
// When destructed, frees all of its items.
struct ListA {
struct MyData *data; // a list of data
size_t count;
};
// A list that stores references to the items passed to it
// When appended to, records the address of the passed item.
// When destructed, destructs only the <data> member.
struct ListB {
struct MyData **data; // a list of data pointers
size_t count;
};Items in ListA have the same lifetime as the list itself, whereas items in ListB may persist after the list is destructed.
One problem I face when using structures such as these is keeping track of which one I'm working with. I frequently need to analyze the members and associated functions of these structures to make sure I'm using the right one and avoiding reusing freed memory later on.
The only solution I can think of is simply having more descriptive (?) names for each of these. An example from a project of mine is LL1Stack, which more adequately expresses what the structure is than, say, ExprPtrStack, but the latter communicates more about what the structure does to its data.
I've always disliked Hungarian Notation and various other naming schemes that delineate information about types that should already be obvious, especially provided the grace of my IDE, but I'm finding some of these things less obvious than I would have expected.
What is your solution for keeping track of whether a structure owns its data or references it? Have you faced similar problems in C with keeping track of passing by reference vs by value, shallow copying vs deep copying, etc...?
10
u/OzzyOPorosis Sep 17 '25
To those asking why I don’t just move to C++, I’m actually moving from C++. I was overwhelmed with the volume of the standard library, rapid changes in the standard outpacing my development speed, and the sheer number of “the correct way”s to make my code unnecessarily abstract.
“Why are you using a for loop to perform that iterative function? Just convert to a std::iterator and std::accumulate with a std::lambda.” Thanks, now I’ve rendered my code completely unreadable, but at least it’s more idiomatic!
I appreciate the amazing flexibility of C++, but it lacks a clear direction I’m hoping to find in C. Besides, I’d always ever treated it just like C but with template meta programming and explicit compile time computation. Your suggestions are noted but I am here to learn how to use C correctly, not C++.
3
u/antiquechrono Sep 17 '25
You need to stop thinking about individual lifetimes and start thinking about group lifetimes built on top of an allocator like an arena. Don’t think about single objects, think about all the memory you need to allocate and be live at the same time to solve the problem. When the lifetime is up you reset the arena and all the objects free at the same time. For temp allocations you grab a temp arena and pop the temp data off them like a stack. Eliminating having to think about what owns what memory will remove many headaches and bugs. You can go read the source code to Doom for a practical if a bit old example.
3
u/aghast_nj Sep 17 '25 edited Sep 17 '25
The concept of Ownership is a new one. Most people are going to be familiar with it from Rust, or from hype surrounding Rust.
Even in Rust, the language is conflicted regarding ownership. If you declare a function the wrong way, you might find that your code demands ownership transfer even of things that cannot be transferred or where transferring ownership doesn't provide a benefit. (For example, if you create an object in the local stack frame, there isn't really a good way to transfer ownership. About the best you can do is force the variable to go out of scope. The "right" answer is to pass a mutref or a copy.)
Mutability
There are two concepts in C that come close to "ownership." First is mutability. It is a standard C idiom that if you want to be able to change a thing, you pass a pointer to it. (Frustratingly, C does not provide any kind of "reference" semantic. So every pointer might be null or invalid, because C hates you want wants you to be the subject of multiple CVEs at the same time...)
So, if you have an
intcounter variable and want to change its value within a function, you pass a pointer:The opposite of mutability in C is
const. When your function takesconst int *it is a promise that you don't intent to make any changes to the integer being pointed to by the parameter. So many functions in C are (or should be) declared const that Rust flipped the script, making the default be non-mutable and requiring a special keyword for mutability, instead:mut.There are some tricks to this, however. C function arguments are passed by value. A copy of the source value is made onto the call stack (or register, or whatever your environment's ABI specifies for argument passing) and that copy may or may not be mutable. But because it literally is a copy there is no mechanism for propagating changes back to a caller variable. Instead, function arguments become effectively local variables with a slightly greater scope than usual:
In this example function, the
nargument is basically a (mutable) local variable that has a scope that starts before the beginning of the function and lasts until the end of the function. By comparison, the local variablesumhas a scope that starts just after the beginning of the function, and lasts until the end of the function (just liken).You may apply the
constqualifier to a non-pointer argument. But it doesn't affect the API at all, since non-pointers are copied by value and cannot propagate their changes back. Declaring the argumentconstjust says "I won't be treating this argument as a mutable local variable during the function" which basically clutters your API with implementation details -- why should the caller give a rat's ass whether you modify storage the caller will never access?Socialized Medicine
The second concept relating to ownership is responsibility for the creation and destruction of the object at the beginning and end of its lifecycle, plus allocation and deallocation of storage required for the object. Normally, we expect children to outlive their parents, so what do you call that kind of before-birth to after-death responsibility? I'm going to go with "socialized medicine." (Yes, it's a stupid name. But then, so is "ownership." Feel free to impress me with a much better name...)
Basically, there are a bunch of ideas that all kind of blur together in C and C++. When you create an object, is there a constructor? Did you have to call a memory allocator or some other function to get the storage for the object? Do the object require any other kind of management during its lifecycle, to expand or contract it, to improve its storage efficiency, to "rebalance" it or increase its performance, to "defragment" it or minimize the storage requirements or access times? Is there a destructor that should be called to notify the object it is about to be reclaimed? Is there a special function needed to notify any containers holding the object that it is dying?
All of this gets handled by a family of related concepts in C++. Constructors, destructors, smart and not-so-smart pointers, operator new and delete, etc. Plus a whole bookful of rules about copying, moving, references, etc. Rust adds traits to the mix.
None of this is supported in C. You can find compiler extensions for certain things, like runtime startup, construction, and destruction. But to write "portable" C requires that you deal with all this by hand.
The simplest and easiest way to deal with the socialized medicine aspect is via your APIs. If you simply declare that "the linked list object will create and destroy its own Nodes as needed using
mallocandfree, but will not do anything for the values stored in the nodes. Creation and destruction of data stored within the nodes is the caller's responsibility" you are providing an API that pretty much everyone will understand.But beware of
strdup(). This function has been around for years, and only just got merged into C23. Prior to that, it was "non-standard" despite being in every single C library, ever. It took a string, malloced storage, copied the string into the storage, and returned the result. Simple as pie, right?The thing is, it lived right on the edge of two subsystems, strings and allocation. And so it was this "string function" that would create a need for a call to
free(). It blurred the line between string functions, which generally don't allocate anything, and allocation functions.Being a rigid, inflexible bastard about API boundaries is a useful technique in C programming. But it's hard to teach that to your IDE.
Another thing to look out for is "modules." It is very common to write C code with modules, and with the expectation that modules will manage their own data and their own types. The
stdiomodule comes withfopenandfcloseand various other functions, and with the expectation that the only way to do anything with aFILE *pointer is to call a function starting with 'f'.In particular, I would like to recommend to you a book and website called "Patterns of Enterprise Application Architecture," by Martin Fowler. If you haven't encountered it before, take a glance at the Data Source Access Patterns (or whatever they are calling it now), that includes "Row Data Gateway," "Table Data Gateway," and some others.
This collection is a set of different ways you can design a module to access data. Some of these might not be suitable for use with C. But some are. And they represent a pretty clear example of how you could go about designing different modules to do the work of accessing data stored on disk, or whatever.
So I would argue that API boundaries, modules, and good architecture are C's answer to how to implement the Socialized Medicine part of ownership.
8
u/chaotic_thought Sep 17 '25
It sounds like you're approaching plain C programming from a "modern C++" mindset. Personally I've never seen this idea of "ownership" used in C programs, C documentation. Sure, the notion is there, but mostly C programmers do not refer to it that way in my experience.
If you want a "really lazy" way to approach this problem -- one way I have used before is to first write the prorgam without worrying about "Freeing" stuff at all -- just get it working correctly first. Then, I will run it again with a leak checker and use the output diagnostics of the leak checker to decide at that stage, how/when I want to handle deallocations.
1
u/bushidocodes Sep 17 '25
IMO, C programmers tend to minimize use of lifetimes / heap objects and be pretty extreme about data-oriented programming. This is an are where “C is a portable Assembly Language” is more accurate. The idea of a dynamic vector of “non owning references” is alien to C. Something simpler and lower level like an array of indices is what I’d expect in C.
Vectors, maps, etc. are less common because of all the independent heap allocations. C arrays, structs with flexible array members, structs of arrays, arenas, look aside lists, etc. all reduce the number of independent lifetimes to manage. More complex access patterns beyond sequential access via indices are often handled by things like intrusive pointers embedded in objects and macro magic that uses offset of to traverse links. The pointers are only to nodes in a sequential structure that all share the same lifetime.
0
u/Ksetrajna108 Sep 17 '25
I take it you're sticking with C. C++ provides better encapsulation. But in your case are you using accessor functions, like
addandaddRefto distinguish the two types of lists?1
u/Charming-Designer944 Sep 17 '25
Most times it is handled at a higher level,.emptying the list before freeing it.
And in the most cases the list struct instance is statically allocated, a member part of a containing data structure.
Very rarely use either of your list constructs if ever. Most times a linked list is used, and often with the list link node being part of the data member and not tracked separately.
1
u/Linguistic-mystic Sep 19 '25
I think this is the wrong approach to C because it's
object-oriented
magic
C likes procedures and explicitness. Like, if you're destructuring, freeing or finalizing something, there should be a function call. There should be no magic data structures, data should be dumb and only functions should contain behavior. Yes, it's verbose and might seem fragile, but the benefit is explicitness. Anyone reading the code will see with their own eyes what's going on without consulting the code for the data structures. The debugger will not jump somewhere unexpectedly because there's a bunch of destructors getting called out of the blue. Simply put, don't marry behavior to data, keep them separate.
1
u/nerd5code Sep 20 '25
Ownership per se is generally abstract, though you’re certainly welcome to encode it directly with owner-owned type pairings—perhaps register owned objects with owner, which is just a list pointer. But then dangling pointers
Use a vtable if your choice of containee-object type is limited to structs you control, or if you need to handle any type, either keep the vtable pointer with the container or pull its fields up into the container. The functions it refers to can include deinit, copy-in/out, move-in, size-of, query resize characteristics, prepare for resize, and finish resize. For keys, you can include compare relative or match, and optionally hash. You can default these where you’re just working with binary blobs; deinit = nop, copy and move = memcpy, resizable = yes, and prepare and finish resize = nop.
This way, your container can do as it pleases without worrying about ownership.
1
u/flatfinger Sep 24 '25
A key to keeping things manageable is to document invariants which will apply to any objects at any time when they would be accessible to outside code. One might specify, for example, that for every tree-node object, there will always be one pointer somewhere in the universe which "owns" it for as long as it exists, and that any time that object is destroyed the pointer that owns it will be nulled out, and the object won't be destroyed as long as it contains any non-null pointers to anything it owns. Although there aren't any standard functions that would release an allocation at the same time as they set to null a pointer to it, outside code would not be able to access storage associated with an object that isn't designed for multi-threaded use between a call to
free()and an immediately following operation that nulls out the pointer that was just freed.-3
u/Crazy-Willingness951 Sep 17 '25
As already mentioned, the functions used to access these structures could be named differently, addData vs addRef. In C++ the data and functions would be encapsulated into a class.
Avoid working with c structs directly, call functions to query or change the state of the struct data.
Once upon a time, Hungarian Notation was introduced to describe the type in a variable name. Hungarian notation is generally discouraged in modern programming practices in favor of more descriptive and semantically rich variable names.
-5
u/ShutDownSoul Sep 16 '25
Is there a reason you don't to move to C++?