I think I only understood much of it once I learned Rust, because you realize: Ah, that thing I once did in C is something that maybe ahouldn't be possible at all without extra steps. Even if I were to nwver use Rust again, this definitly helped to understand how to use pointers more safely.
Exposure to an assembler makes pointers easy to understand.
IME languages like Python aren't any easier than C to work with (ignoring UB issues of course), but it's certainly the case that you can probably kinda sorta get your job done with Python even without understanding the first thing of what you're doing, and that's not happening if you write in C.
- typedef struct { ... } foo
- foo *foo_create()
- void foo_destroy(foo *)
- a bunch of functions that take foo* as their first arg
which is kind of the same as a class and only more error-prone.
I say this as someone who actually _likes_ C, but the manual memory management model is very often unnecessary, confusing, repetitive. There was an idea some time ago of a language extension that would extend the concept of automatic storage duration to allow an explicit destructor to be called when the variable goes out of scope, like <close> variables in some languages. I genuinely think things like that would make the language a bit more ergonomic without fundamentally changing its nature.
foo f;
foo_init(&f);
foo_destroy(&f);
...
foo *g = malloc(sizeof(foo));
foo_init(g);
foo_destroy(g);
free(g);I would write the allocation as
foo * const g = malloc(sizeof *g);
to avoid repeating the type name and "lock" the allocation to the variable. If the type on the left hand side ever would change, this still does the right thing.In GPU programming, there are actual different memory sub-systems, with different sizes and different performance implications, so it’s critical that the caller is able to do the allocation & deallocation any way they want. This is why most well designed GPU APIs rarely allocate GPU memory inside the API, but instead are designed to work with caller-provided pointers to buffers.
In the case you show, foo would have to be a struct that doesn't contain pointers to additional allocated memory, but its a entirely valid use case and pattern.
Calling malloc and free, has a cost associated with it, that the stack doesn't. But stack can in some cases, like with recursion be scary to use, because you dont know where it ends. If malloc returns NULL you know you have found the end and can do something reasonable.
Thanks for you insight!
IIRC clang implements it as well, but I wasn’t able to find a reference to it in their docs.
There is a much more advanced design and implementation at “A defer mechanism for C” (December 2020): https://gustedt.wordpress.com/2020/12/14/a-defer-mechanism-f...
For my own purposes, I think I can live without handling stack unwinding so I continue working on my pre-processor.
Since the pre-processor is not yet finished, there I use a vector¹ of {.pointer, .destructor} where I put objects after initialization, with one macro at the end of each managed scope that calls the destructors for that scope in reverse order, then another macro meant for the function exit points that calls all the destructors in that vector. This has been built many times before by other people of course, it’s just an exercise to see which difficulties arise.
¹ Vector, growable array: I did my own trivial type-generic growable array, with the classic {.capacity, .len, .items}, but again there are many previous implementations. The two I’ve found more interesting are:
- “C Template Library (CTL)”: https://github.com/glouw/ctl
- “Klib: a Generic Library in C”: https://github.com/attractivechaos/klib/
https://github.com/tezc/sc/tree/master/array
It is just an array of your type, e.g int *numbers, so you have type info in debugger as well.
What do you mean by broken alignment?
The alignment is broken because nothing in the C standard guarantees that the elems member of sc_array will be aligned correctly for any possible element type.
I also spotted another problem, in sc_array_init the code `void *p = a` is also not guaranteed to work. In an example snippet such as `int iv; sc_array_create(iv, 0);` expands to `sc_array_init(&iv, sizeof iv, 0)` so the type of the expression `&iv` is `int *` which is then being converted to `void *` in the function which is actually not allowed by the standard. This is also the reason why if you were writing a wrapper around realloc which exited if the allocation failed you would still have to pass in the current pointer with void * and return the new pointer with void *. This could be applied here actually as an easy fix but it indicates even further to me that the author of the library is taking a very leisurely approach to writing conforming C. This pattern also appears in the other two functions though and I'm not sure if in those cases it's something which can be easily fixed.
#define P
#define T int
#include <vec.h>
#define T vec_int
#include <deq.h>
A deq_vec_int - analogous to std::deque<std::vector<int>> - is a neat example. struct X { int a; }
struct Y { *X; int b; int c;}
void add(Y* self, int number) {
self->a += number;
}
Y y;
y.a = 10; // composition
y.add(1) // y.a = 11 now
This alone would simplify C coding so much without taking any power out of it.The other extension i would add is some sort of interface or protocol.
As soon as you can do something like "y.add(1)", having a generic contract to refer to things without having to know its concrete type is some of the good things from the OOP world.
With this you would also be able to call some cleanup code and even a initializer.
This is still C and its still much simpler than C++, and yet almost as powerful.
C should propose these kind of things even if it was not that conservative and it would retain a lot of coders that migrate instead giving C barely evolved from its 70's roots.
Unless you decide you use libgc, presumably?
a = {"one": 1, "two": 2 }
b = a
b["two"] = 99
print(a["two"])
The above prints 99, since "b = a" does not copy the value (the dictionary) but just the reference to the value ("the pointer", kind of). This is surprising to some people.I know it's probably baseless, but I can't shake the feeling that people who learn modern languages before learning C are just making their own lives harder.
[0] https://www.instructables.com/CARDIAC-CARDboard-Illustrative...
# hash
my %a = ( 'one' => 1, 'two' => 2 );
my %b = %a;
$b{ 'two' } = 99;
# prints 2
print $a{ 'two' }, "\n";
# reference to hash
my $a = { 'one' => 1, 'two' => 2 };
my $b = $a;
$b->{ 'two' } = 99;
# prints 99
print $a->{ 'two' }, "\n";Most dynamic languages expose the data as references. In fact, the one thing that trips up JavaScript developers (especially in React) is that they do not understand how references work. I see senior and lead developers inadvertently doing mutation all the time. Or getting incredibly paranoid that two identical strings, for example, do not equal each other in the strictest sense in JS. They also throw in memoization everywhere due to their fundamental lack of understanding.
You can always tell the developers that do not have C/C++/Pascal experience.
[1] https://en.wikipedia.org/wiki/Sigil_(computer_programming)
def foo(mylist=[]): mylist.append("a") return mylist
mylist is only initialized once, so the function will actually return one more "a" with each function execution
def foo(x, cache={}):
if x in cache:
return cache[x]
val = cache[x] = x*x+1
return val
But, be warned, there's no mechanism for cache eviction; use @lru_cache if you don't know ahead of time that x will take a reasonably small number of valuesHere is a great “rant” at it in case of Java:
https://www.ralfj.de/blog/2018/07/24/pointers-and-bytes.html
Same goes for arrays, `int *x[30]` just says that `*x[30]` is an `int` (well, technically it's out of bounds by 1), thereby we're declaring an array of 30 int-pointers.
Of course, that hasn't been entirely true since function declaration changed with ANSI C in the late 80s or so.
When using that variable with the allowed operators, the resultant type of the expression is determined by the declaration:
pa; // int *[10]
pa[x]; // int *
*pa[x]; // intAnd unary operators on an identifier are a different thing, though I similarly don’t think adding one at the left, another at the right is a good thing. But I have trouble with the declaration site as well.
It’s getting out of reach of my knowledge, but I believe C’s grammar being hacky is in part due to that as well.
I didn’t find exactly what I was looking for but here is some more on it: https://pdos.csail.mit.edu/archive/l/c/roskind.html
int a, *b, (*c)(int); int * a, b;
Here it sticks to a.Then typedef comes along and ruins everything:
typedef int * pint;
pint c, d;[1] https://www.atlasobscura.com/articles/swazzle-punch-and-judy
C's pointers aren't memory addresses. Ok, they tend to be represented as such at run time, but that's not what the spec actually says the are. And as far as compiler authors are concerned, they can do anything they want as long as it's within spec. Further, the spec even requires some additional behaviors pure memory addresses aren't capable of. See https://www.ralfj.de/blog/2020/12/14/provenance.html for examples of the extra requirements.
Compared to that mess, lambdas are trivial. They're just functions.
I see that as a European, I have virtually no chance to understand pointers using street numbers. :)
(Fortunately I've never had problems either with lambdas or with pointers.)
I've always thought that an introduction to CPUs (can take a simpler one as example) and how they work, how memory is (usually) organised, and to assembly would go a long way in helping understand many programming issues and C.
My experience is that C or programming concepts are often taught in a very abstract/mathematical way, which can be hard to grasp compared to a more practical approach.
If you take a concrete example where memory is effectively an array and indices are addresses (which holds true for most cases and, in any case is a good example) then understanding pointers becomes basically common sense and notations are simply conventions of the language you're using.
Thank you for your reply...
The "very abstract" way C is taught actually prevents people from making such assumptions by not priming them to make them. The fact that people get complacent and start to lean on their understanding of (what they think are) real machines to write C is the result of the bugs I mentioned in the previous response.
https://www.miasap.se/obnc/oberon-report.html
http://people.inf.ethz.ch/wirth/Oberon/PIO.pdf
If you know how to walk down a street and stop at the right street number, then you have used pointers. And if you've ever observed that one tall building may "cover" a range of street numbers, such as 200-220, then you should understand how to move from one 4-byte "value" to the next in an array in memory.
Anyway, many more analogies... probably better than this one.
Maybe unions could make using pointers a bit more challenging, but again, tall buildings next to short buildings and so on. We do this kind of pointer calculation in real life.