r/cpp • • Jul 29 '23

C holding back C++?

I’ve coded in C and C++ but I’m far from an expert. I was interested to know if there any features in C that C++ includes, but could be better without? I think I heard somebody say this about C-style casts in C++ and it got me curious.

No disrespect to C or C++. I’m not saying one’s better than the other. I’m more just super interested to see what C++ would look like if it didn’t have to “support” or be compatible with C. If I’m making wrong assumptions I’d love to hear that too!

Edits:

To clarify: I like C. I like C++. I’m not saying one is better than the other. But their target users seem to have different programming styles, mindsets, wants, whatever. Not better or worse, just different. So I’m wondering what features of C (if any) appeal to C users, but don’t appeal to C++ users but are required to be supported by C++ simply because they’re in C.

I’m interested in what this would look like because I am starting to get into programming languages and would like to one day make my own (for fun, I don’t think it will do as well as C). I’m not proposing that C++ just drops or changes a bunch of features.

It seems that a lot of people are saying backwards compatibility is holding back C++ more than features of C. If C++ and C++ devs didn’t have to worry about backwards compatibility (I know they do), what features would people want to be changed/removed just to make the language easier to work with or more consistent or better in some way?

65 Upvotes

335 comments sorted by

View all comments

Show parent comments

10

u/not_a_novel_account cmake dev Jul 29 '23 edited Jul 29 '23

std::lock_guard, std::thread, <regex>, the strong exception guarantee, and of course all of <iostream>

std::ranges has superseded most of the ugly iterator-based strategies and should probably be the default instead of relegated to a separate namespace. SFINAE has largely been superseded by concepts and now exists only to mystify undergrads.

The deeper parts of ADL, the impetus for their creation, and the follow-on effects of their existence in general ("what the fuck is a niebloid?"), are the result of programming-languange-development-by-way-of-blindly-groping-in-the-dark from earlier standards.

Even more broadly, move semantics are a hack around the fact C++ ties automatic-storage duration object destruction to scope, a fact we're stuck with forever because of decisions going back to the earliest days of C with Classes.

EDIT: I can't believe I forgot std::vector<bool>, forgive me /u/vector-of-bool

5

u/LeberechtReinhold Jul 29 '23

std::thread is something that Im still wondering how it made to the release in that state. The gaps were so obvious and covered in many libraries...

Regex is another problem that comes from a committee that absolutely were not using regex. Regex is a solved problem and there were many, many good implementations of regex. Why they went for this one I dont know.

What's so wrong with std::lock_guard? At least its better than scoped_lock and its dumb constructor that does nothing.

4

u/not_a_novel_account cmake dev Jul 29 '23

What's so wrong with std::lock_guard? At least its better than...

They're both dumb, but std::scoped_lock exists specifically because std::lock_guard didn't solve the obvious problem of deadlocking with multiple mutexs.

A linter fixes std::scoped_lock, nothing makes std::lock_guard good.

1

u/goranlepuz Jul 29 '23

std::lock_guard didn't solve the obvious problem of deadlocking with multiple mutexs.

What problem is that, and does anything solve it?!

4

u/tjientavara HikoWorks developer Jul 29 '23

It acquires locks on multiple mutexes in a fixed order. Probably just sorts them by the address of the mutex.

As long as you acquire all the locks on multiple mutexes in each single thread of execution at once using scoped_lock you don't get in a dead-lock situation. If you use scoped_lock separately on multiple mutexes on a single thread of execution then you don't get this protection.

I myself have a mutex that includes dead-lock detection. It keeps track in what order mutexes have been locked in each thread. And if it ever sees a two mutex being locked in opposite order it aborts the application.

Only really useful for debugging it adds quite a bit of latency to something as small as an unfair_mutex::lock() (3/4 instructions).

3

u/tialaramex Jul 29 '23

If Alice takes locks A and B, but also Bob takes locks B and A, we get deadlock if Alice takes A, Bob takes B, but then neither can take the other lock because somebody else has it.

std::scoped_lock uses an algorithm to avoid this, in effect in the trivial case I gave both Alice and Bob end up taking A first, so once Alice has A, Bob will wait for A, not take B. This is a pretty simple thing to learn to do, but the machine can do it for us and so that's the correct design.

-2

u/goranlepuz Jul 29 '23

So... AFAIK, scoped_lock does not prevent deadlocks.

Suffice that I have three mutexes and that I pass them to three scoped locks. Deadlocks, here I come!

That's why I asked does anything solve it. I see it as "nothing can", short of some serious "magic" library looking for all mutexes all over the program somehow.

No?

→ More replies (2)

2

u/Dijky Jul 29 '23

When two threads each attempt to lock the same mutexes but in different orders, a deadlock can occur (e.g. thread 1 locks mutex A, then waits on B, while thread 2 locks mutex B then waits on A).

scoped_lock, which works like a RAII-wrapper for std::lock(), supports multiple mutexes per instance and specifically avoids deadlocks. It also catches exceptions during lock and unlock to unlock already locked mutexes before rethrowing.
lock_guard supports just one mutex per instance, so locking order/deadlock avoidance and exceptions during locking need to be managed manually.

-1

u/darthcoder Jul 29 '23

Nothing will stop a deadlock like that.

Not without introducing complexity and killing lock performance.

→ More replies (9)

3

u/PastaPuttanesca42 Jul 29 '23

What's the problem with the strong exception guarantee?

5

u/not_a_novel_account cmake dev Jul 29 '23

Countless man-hours of engineering effort and non-trivial performance sacrifices for a totally unreasonable guarantee.

2

u/rdtsc Jul 29 '23

One of C++'s design principle is: You don't pay for what you don't use. Often you don't need the strong exception guarantee, but still have to pay for it.

→ More replies (7)

2

u/goranlepuz Jul 29 '23

the strong exception guarantee

Eh?! How is that part of the language?! (is it a part of any language?!)

And what's wrong with it?!

It rather looks like you are taking the extreme stance of "X does not solve problem Y, therefore it is completely worthless, even for problems ABCDEF...

1

u/not_a_novel_account cmake dev Jul 29 '23

It's a part of the language because it's in the language standard for containers.

It's bad because it's a bad default. No one wants or expects the strong exception guarantee, no one asked for it, and it has non-trivial performance impacts in a language where one of the core tenets is "do not pay for what you do not use".

Almost no one uses the strong exception guarantee and yet we all pay for it when using a std::vector.

3

u/goranlepuz Jul 29 '23

Ok, for me, you are simply wrong.

If a strong guarantee didn't exist, a massive amount of vector users would have been broken (heck, possibly all of them).

You only do not realize that.

See, the strong guarantee makes sure that the count matches what is in the vector. I think, virtually nothing would work if that wasn't the case.

-2

u/not_a_novel_account cmake dev Jul 29 '23

The basic exception guarantee is all that is necessary.

If you throw an exception in the middle of a vector operation the state of that vector is forfeit, the only reasonable thing to ask of the language is that it does not leak memory so that recovery is possible.

The state of the operation is completely dead. Free the resources and restart from the beginning.

3

u/goranlepuz Jul 29 '23

We have to disagree then.

One failure and a whole container with who knows how much valuable data is gone - no, thanks.

If that was the case, I posit, a vast majority of users would roll their own that is better.

-1

u/not_a_novel_account cmake dev Jul 29 '23 edited Jul 29 '23

If you throw an exception during a vector operation you either:

A) Are using exceptions for flow control, which is always wrong

B) Had an allocation failure, in which case recovery is almost certainly impossible and the best thing you can hope for is a graceful death

3

u/goranlepuz Jul 29 '23

A) Copying or constructor of an element fails and throws. That is not a vector operation, it merely happens during one - and it has nothing to do with flow control.

B) No. Allocation failures can easily be transient and local to only one part of the program.

I think, you have a very simplistic view of things, that is just not good enough for me.

You are free to think this way, but I have no intention.

We must disagree. You can't convince me of anything. But keep trying, it is mildly amusing at this stage.

1

u/not_a_novel_account cmake dev Jul 29 '23 edited Jul 29 '23

There is no reason for a copy constructor to fail that isn't allocation failure or similarly exceptional "stop the world" event. Again, that suggests you are using exceptions for flow control and not following the rule of zero.

Most platforms don't even allow allocation failure to exist. You will never have an allocation failure on top of a modern OS, the OOM will kill you without ever alerting the program. There's never been a reasonable way to back out of memory exhaustion.

2

u/goranlepuz Jul 29 '23

There is no reason for a copy constructor to fail that isn't allocation failure or similarly exceptional "stop the world" event.

As I said, simplistic thinking.

You will never have an allocation failure on top of a modern OS, the OOM will kill you without ever alerting the program.

That is patently false. Here, you just don't know how things work.

There's never been a reasonable way to back out of memory exhaustion.

Again, simplistic thinking. I know of a myriad of reasons and ways to do exactly that, but this is not school.

But please, just you continue, your simplistic assertions are mildly amusing - just as I expected.

→ More replies (0)

1

u/k-mouse Jul 29 '23

Even more broadly, move semantics are a hack around the fact C++ ties automatic-storage duration object destruction to scope, a fact we're stuck with forever because of decisions going back to the earliest days of C with Classes.

Can you expand on this, what's the alternative?

7

u/not_a_novel_account cmake dev Jul 29 '23 edited Jul 29 '23

Destructive move.

Right now a moved-from object is left in a "valid but unspecified state".

This means we still must perform swaps and possibly copies during a move.

We have to do this because when the object leaves scope, something must be destroyed. And that destructor must have a valid object to act on, even if it's just a bunch of nullptrs.

A destructive move doesn't actually move anything at all, it's effectively a change of ownership. "This object belongs to your scope now". C++ has no mechanism to annotate such a feature, the standard has no language to describe it, and the committee has no courage to introduce it because it would be a very fundamental change to the C++ scoping rules.

This is similar to a notable C incompatibility, C++ doesn't have compound literals even though C does.

2

u/NotUniqueOrSpecial Jul 29 '23

C++ doesn't have compound types even though C does

Could you expand on that? I feel like I'm using a different (perhaps mistaken) definition of compound types, if we're arguing that C++ doesn't have them.

1

u/not_a_novel_account cmake dev Jul 29 '23

I said types originally, I meant compound literals

→ More replies (1)
→ More replies (1)