
The problem with C - fanf2
https://cor3ntin.github.io/posts/c/
======
grive
It's weird to conflate C as a language and its ABI, as the glue between other
languages. The glue in question is the ABI, period. Rust does not interface
with C, it interfaces with a C library (dynamic or static) that is in a
standardized format.

C++ attempts something messy, which is to take C source and generate objects
conforming to this ABI. In doing so, the only sane thing to do is to respect
the C spec to transform C source in a binary blob that others can understand.

C++ should stop attempting to parse C. Just use a C compiler, create a binary
blob and interface with it from the C++ compiler. Attempts to make both
language compatible on some common subset makes them both worse off.

~~~
flohofwoe
IMHO C++ should continue to be able to parse C declarations (e.g. include a C
header which only has declarations in it - in fact, _all_ languages with C
interoperability should have this feature), but otherwise I agree, C++ doesn't
really need to be able to _compile_ C code because (usually) it's trivial to
setup a mixed project where C source files are compiled with a C compiler, and
C++ source files are compiled with a C++ compiler.

This requires that the Microsoft C compiler is updated to the latest C
standard though. Until recently their recommendation was to compile C code
with the MSVC C++ compiler.

~~~
scatters
A C header can contain function definitions, though - either using the inline
keyword (since C99) or as macros. Moving all functionality behind a function
call barrier may not give satisfactory performance.

~~~
flohofwoe
Yeah, adding the inline keyword to C wasn't such a great idea in hindsight
because it muddled the separation of interface and implementation (LTO is the
better solution IMHO). A possible workaround would be that such a "declaration
parser" would ignore the function implementation block (but inline code would
make the library unusable from C++ anyway). As for macros: they would simply
be resolved by the declaration parser, and either the result is parsable as
declarations or not (which would then be an error).

~~~
i-am-curious
Doesn't LTO interfere with debugging massively?

------
SomeoneFromCA
As someone who writes a lot of C-like-C++ (system software, network daemons),
I absolutely disagree. Using a template or a destructor here and there allows
you write C++ code, which is as fast pure C, safer, and easily understandable
to a C programmer.

~~~
jstimpfle
I doubt it's really safer at that point.

You could use a few vectors or other container objects, which is a tradeoff -
it gets a little bit more comfortable to spot/debug OOB issues (but valgrind
and sorts can also help with the occasional problem). In exchange you are
required to structure your data and code in a particular way to use these
structures, which is a drawback. Another drawback is slower compile times
(significantly so with std:: containers -- don't have a lot of experience with
hand rolling C++ abstractions).

I often prefer having globally allocated (dynamic) arrays, which are
unproblematic with respect to OOB or memory/allocation safety. I use a simple
macro to wrap allocation of arrays to remove the boilerplate. It becomes like
"realloc_array(pptr, FooType, 42)" or even "realloc_array(pptr, 42)". Local
allocations are more often not easily dealt with using Memory Arenas, which is
superior to alloc/free in constructors/destructor.

I even prefer using plain pointers with separate size variables for dynamic
arrays. That's because "size :: array" isn't a 1::1 relationship. More than
50% of the time, I have more than one array that applies to the same size
variable. Sometimes, there are multiple index/size pairs that apply to a
single (or multiple) arrays.

I prefer staying with C because it keeps me focused. Sure, sometimes I wish
for the occasional fancy feature, but then taking the time to think about a
clean approach that solves the problem with limited features often benefits
code quality and readability.

~~~
jcelerier
> I doubt it's really safer at that point.

the number of bugs that the compiler has saved me by e.g. having a simple
Id<T> type tag instead of ints as used by every C library under the sun is
absurd.

~~~
jstimpfle
I don't know, I've tried approaches like that, even played with writing a few
C++ helper functions, and even started a compiler with the goal of exploring
ideas like this.

How do you go about Id<T>? The way I see that something like this can work out
and give small advantages is that T would be an _abstract_ type (a phantom or
tag type, or whatever C++ guys are calling it). And all arrays that are meant
to be indexed using Id<T> (while the array element types could be whatever)
would depend on this type and have to be implemented using a special wrapped
array type with overloaded operator[].

I don't know, this might come with many disadvantages.

All the stuff goes out the roof when you're now dealing with start indices +
slices, i.e. doing "pointer" arithmetics.

My general feeling is that this Id<T> approach might show problems similar to
the idea of using typed units, like wanting to prevent the expression "1W +
3s" but then making it terribly cumbersome to do real work, e.g. "1W * 3s"
which would be perfectly valid.

In one C project I resorted to using about 20-30 typedefs like:

    
    
        typedef int Expr;
        typedef int Symbol;
    

etc. And I used these typedef names in function signatures. Variables also
were named like expr, parentExpr, symbol, etc. It worked wonderfully. I had
maybe 1 mistake where I submitted the wrong "type" in 6 months. And I'm
someone who makes a lot of stupid mistakes.

~~~
jcelerier
> How do you go about Id<T>? The way I see that something like this can work
> out and give small advantages is that T would be an abstract type (a phantom
> or tag type, or whatever C++ guys are calling it).

in a very simplified way:

    
    
        template<typename T>
        struct Id { int value; };
    
        template<typename T>
        struct Identifiable { Id<T> id; };
    
        struct MyEntity : Identifiable<MyEntity> { string name; void foobar(); };
    
    

> And all arrays that are meant to be indexed using Id<T> (while the array
> element types could be whatever) would depend on this type and have to be
> implemented using a special wrapped array type with overloaded operator[].
    
    
        std::unordered_map<Id<T>, whatever> 
    

works fine (or your container of choice... flat_map, boost::multi_index...).

> It worked wonderfully. I had maybe 1 mistake where I submitted the wrong
> "type" in 6 months.

well, you're good, I remember when I introduced this in my code base, that
caught a dozen bugs almost immediately - and I get prevented by the compiler
to introduce more every couple months.

------
Nginx487
It's a widely spreaded misconception, that C++ is a superset of C. Nope.
Modern C has ton of features, which are impossible in C++, from loosening
requirements to implicit type casting (pointers to int) to main() recursion.
They are different languages with different niches. I program both in C and
C++, and honestly, I don't see modern application of C outside embedded, where
C++ compilers implementation is impractical. C++ currently widely used in OS
and drivers development, except Linux kernel of course, the only reason of
that design approach is personal opinion of Linus Torvalds.

~~~
i-am-curious
> I don't see modern application of C outside embedded,

C is one of the most popular languages for high-performance code. Most famous
example would be the Linux kernel but literally anything that needs the best
optimizations possible eventually comes down to C.

~~~
Nginx487
I worked under ULL trading platform, they used C++ with heavy templates,
without RTTI and exceptions. If things can be done in compile time, it should
be done - which is the purpose of templates. Neither our nor neighbor teams
(near 800 developers) used plain C. As Bjarne Stroustrup told, there's no
place between C++ and machine code for "more low-level language", everything
which could be done in C, also could be done in C++ with the same efficiency.

Good tendency however, now both C and C++ developers started experimenting
with Rust, probably creating unified community and platform.

~~~
mehrdadn
With the caveat that I am NOT suggesting this justifies choosing C over C++, I
just wanted to mention this talk about how "zero-cost abstraction" is an
idealism, not necessarily a reality:
[https://www.youtube.com/watch?v=rHIkrotSwcc&t=17m30s](https://www.youtube.com/watch?v=rHIkrotSwcc&t=17m30s)

That said, I tried to reproduce something similar, and it seems the issue only
occurs in my example due to external linkage (adding 'static' fixes it)... but
I can't claim this will always resolve the issue:
[https://gcc.godbolt.org/z/1vbqo3](https://gcc.godbolt.org/z/1vbqo3)

~~~
bregma
The "zero-cost abstraction" concept is that you don't pay for something you
don't use. If you use a smart pointer, you pay for it. If you don't use a
smart pointer, it's zero cost.

Any argument that smart pointers are not zero cost to the language because
they have a cost when you use them is a classic straw man argument.

~~~
mehrdadn
I think there might be a terminology mixup here but he probably meant zero-
_overhead_ abstraction. And in any case, the point he's making is not a
strawman argument.

~~~
bregma
OK, let's substitute "overhead" for "cost in the argument.

Premise one: C++ has zero overhead for most of its features, like smarts
pointers: if you don't use them, they cost nothing.

Premise two: using features like smart pointers adds a cost to your C++
program.

Conclusion (due to the strawman logical fallacy): C++ is not zero overhead
because there exists a feature that has a cost if you use it.

I'd like to see the reasoning that leads to the same conclusion without
resorting to the strawman logical fallacy.

~~~
mehrdadn
He talked about more than just unique_ptr if you actually watch the video, but
I don't even get why you're dying to have such a pointless argument here. When
you see free lunches offered somewhere, do you start arguing with people how
everyone is _wrong_ and there _is_ such a thing as a free lunch too? Is it so
hard to actually take the point and just move on instead of dissecting it like
a mathematical theorem?

------
quelsolaar
Good article! I very much agree about C and C++ being philosophically
different. When ever someone asks me a C programmer if I want to switch to
Rust, I think “but thats a replacement for C++ not C”.

I think the ship has sailed on proper compatibly, and thats ok with me, let
them be different. More compatibility is not worth braking any existing code.

~~~
MaxBarraclough
> When ever someone asks me a C programmer if I want to switch to Rust, I
> think “but thats a replacement for C++ not C”.

What do you make of Zig?

~~~
quelsolaar
I think It has some interesting ideas but many of them dont need a new
language. I also think that Zig like C++ thinks that the language is the
problem that needs solving.

As a C programmer I dont want clever features. I want the simplest possible
where nothing happens without me explicitly typing it. I'm much more
interested in better debuggers, and tool chains [1]. Every time you create a
new language or a significant new version of a language, you have to start
over with the tool chain.

[1] [https://youtu.be/pvkn9Xz-xks](https://youtu.be/pvkn9Xz-xks)

------
InfiniteRand
A lot of times for a quick program I end up writing essentially C + C++
standard library, because a lot of C++ features related to better organizing
of code are overkill when you have only a few files, but avoiding manual
string management makes it worth using g++ instead of gcc

~~~
nikki93
I think you can get far with this type of code organization, and using methods
just for easier name lookup of functions associated with a particular type
(static lookup -- no vtables / RTTI). Function overload sets are also useful.
Libraries like
[https://github.com/skypjack/entt](https://github.com/skypjack/entt) really
help here too--for an 'ontology of alive objects' its felt like using an
entity-component-system has been more ergonomic vs. a big tree of inheritancey
classes.

------
sys_64738
A more appropriate question is: The problem with C++ compatibility in C++. The
C++ I wrote 20 years ago doesn't look anything like C++ of the modern day. The
C is the same.

~~~
colejohnson66
I’ll admit I’m partial to C++, but couldn’t one argue that that’s because the
C committee is just slow to add features to improve the language? I’ll admit,
there’s _a lot_ in the C++ standard library, but with zero cost abstractions,
if you don’t use, say, `std::map<T,U>`, you don’t pay for it.

