Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411430 Posts in 69363 Topics- by 58416 Members - Latest Member: JamesAGreen

April 19, 2024, 10:03:00 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)What modern features you want to see in C++?
Pages: 1 2 3 [4]
Print
Author Topic: What modern features you want to see in C++?  (Read 6211 times)
powly
Level 4
****



View Profile WWW
« Reply #60 on: October 16, 2019, 12:54:11 PM »

Usually RAII is talked about in the context of smart pointers. In this case you're dealing with reference counting, and in effect, garbage collection.
Unless it's a unique smart pointer whose only function is to call that free at the end of the scope, which was my entire point. For example I have these written up for OpenGL objects, where it just calls the glCreateWhatever() on construction and glDeleteWhatever() automatically after the scope ends. No reference counting.

And the reality has a lot of downsides that could be avoided with better language designs, or, with strong coding guidelines.
Indeed, the language is quite a mess. There are attempts at guidelines, but it's hard to get everyone on board and it shifts much of the responsibility of "knowing what they're doing" to the programmer -- and the language is so complex that next to nobody knows what they're doing with absolute certainty (I certainly don't.) C has the inherent advantage of being simpler and thus more understandable, but the other side of the coin is the absence of many nice convenience features. If only we could have it all..
Logged
qMopey
Level 6
*


View Profile WWW
« Reply #61 on: October 16, 2019, 01:30:23 PM »

Usually RAII is talked about in the context of smart pointers. In this case you're dealing with reference counting, and in effect, garbage collection.
Unless it's a unique smart pointer whose only function is to call that free at the end of the scope, which was my entire point. For example I have these written up for OpenGL objects, where it just calls the glCreateWhatever() on construction and glDeleteWhatever() automatically after the scope ends. No reference counting.

That's all fine, and makes sense, and is indeed not going to have the same implications as reference counting. You are right about those points.

Though I am personally more concerned with the hidden nature of lifetimes listed in my two previous points 1 and 2.

And the reality has a lot of downsides that could be avoided with better language designs, or, with strong coding guidelines.
Indeed, the language is quite a mess. There are attempts at guidelines, but it's hard to get everyone on board and it shifts much of the responsibility of "knowing what they're doing" to the programmer -- and the language is so complex that next to nobody knows what they're doing with absolute certainty (I certainly don't.) C has the inherent advantage of being simpler and thus more understandable, but the other side of the coin is the absence of many nice convenience features. If only we could have it all..

So far the best thing I have seen, both in my personal code and at work, is to use C++ with limited features. Some rules, for example, no exceptions, no multiple inheritance, and so on. I think this is as good as we can get without a new language. A language definition has the advantage of forcing everyone to be "on the same page" in a very effective way.

So when all is said and done, I find myself at a competitive advantage over other teams that use modern C++. We have an easier time maintaining our code, onboarding new team members, and have happy customers and consumers of our SDK/APIs.

I learned all of my current opinions simply by analyzing the style of APIs out in the wild that either myself or my team has had the easiest time consuming, and have successfully used to ship products. Time and time again it is always the APIs that have A) good docs, and B) C-style APIs. Nearly none of this stuff is my own unique idea, I'm just learning from observation and adapting over a long span of time.

A quick list of some APIs that are ubiquitously lauded for their quality.

- STB libraries
- SDL2
- Box2D
- Granny 3D
- GLFW
- Dear ImGui
- SFML

Probably the best example is Box2D, written clearly in C++ but with heavily restricted feature set. Since the code is so simple in terms of the features used, it is readily portable to other languages. If Box2D were written in modern C++ it would likely not be ported anywhere, and a competitor would have arose and took its place. The author himself talks about these points.

All of these APIs have extremely minimal dependencies and make use of as simple of C/C++ features to get the job done. The underlying philosophy driving the design of these libraries can be summarized as: "only as complicated as necessary". This idea transcends all the way down to which C++ features are used. Contrast this with other popular C++ libraries and the difference is clear as night and day. A typical popular C++ library will:

- Compile slowly
- Not run on an embedded system without tremendous effort
- Non-trivial to port to other language due to the myriad of C++ nuances
- Uses a ton of extra memory, like duplicated strings (I'm looking at you, std::string)
- Giant dependencies (this is the worst of all), resulting in too many problems to list here

Just a random example. At work I have to compile Caffe2 from source. Each cpp file takes 5+ seconds to compile, and they have a lot of cpp files. Why? Because the authors wrote typical modern C++ without understanding anything about compile times. They followed all the rules of OOP, and used plenty of modern C++ features, and yet here we are stuck in the stone age of compilation times. It is not 5+ seconds out of necessity. This is not the way such a large project has to be. That time could be cut down to nearly instantaneous by reducing dependencies in each translation unit, and by merging extraneous translation units together.

The problem is how it's nearly impossible to refactor a large codebase written in modern C++ to reduce compile times after the fact... The only way to solve this problem is with foresight, with the correct code design from the beginning.

Yes, obviously if Caffe2 were written in C the compile times would likely only be marginally faster. Yes. That's not really my point here though. I'm not saying "use C", I'm criticizing the dogma around modern C++. Using a more C-style subset of C++ is merely my solution to actively pressing against this kind of dogma. I'm sure there are plenty of other solutions out there, but trust me, utilizing the next variation of C++ to come out in a couple years will *not* solve the kinds of problems I'm talking about here.



This header tells the story clearly. This is just some random header. I don't even know what this header is for. They are all like this.

https://github.com/pytorch/pytorch/blob/master/caffe2/observers/runcnt_observer.h

Including a header swarm, using multiple inheritance, ubiquitous template usage. These spike compile times, and the inheritance one can have dramatic effects on link time.

It's objectively poorly written code, as far as build time goes, despite being written with utmost care to uphold canonical OOP and modern C++ pillars.
« Last Edit: October 16, 2019, 02:11:02 PM by qMopey » Logged
Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #62 on: October 16, 2019, 10:31:09 PM »

Maybe it's just me, but in my bubble a lot of people would agree with you: OOP is not a good tool in many usecases. So I feel like this derails the argument: C has no builtin OOP support, C++ does, OOP is overused and mis-applied often, so C++ is bad over C? Hm. I don't follow that.

Compile times are a huge burden, though, which it shares with C due to the nature of includes. Having used C# or Java occasionally I really miss that "press F5, watch half a second of files run by, THE THING starts" workflow. Visual Studio's Edit&Continue helps a bit, but still...
Logged

Snake World, multiplayer worm eats stuff and grows DevLog
Daid
Level 3
***



View Profile
« Reply #63 on: October 16, 2019, 10:52:10 PM »

Oh, but that part I agree with. I call it "Java programming mentality", as the java standard library is the first time where I noticed this. And that people where "copying" that by example. Abstractions and interfaces everywhere. And, I hate that as well.

I'm a huge advocate of KISS. And that can work with OOP just fine. Most people that I've encountered that where against OOP where actually against over abstracting. And where just doing KISS OOP in C. I just do KISS OOP in C++. Most of the good examples you mention also do KISS OOP.


Java did get it right not to allow multiple inheritance. I did that in my first larger game project, and it was just a pain in the ass without any real benefit. So I stopped doing it. Wouldn't mind a compiler flag that right out bans multiple inheritance.


(And for every bad C++ example I can throw you a bad C example. But let's not start slinging pOOP ;-) )


Quote
For example I have these written up for OpenGL objects, where it just calls the glCreateWhatever() on construction and glDeleteWhatever() automatically after the scope ends. No reference counting.
Small note of warning on that. glCreate* and glDelete* can be expensive in time. And also, especially for the glDelete*, it is only allowed if the context is active. With RAII you can run into the issue that you are deleting this object when the context is not active. Or you are on the wrong thread.
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
Daid
Level 3
***



View Profile
« Reply #64 on: October 17, 2019, 04:23:16 AM »

Just grabbing this quote to clarify something:
RAII for releasing memory (for example with a std::unique_ptr) is the direct "non-manual" counterpart for new/delete (so for simple types, malloc/free). It has no run-time overhead, at least if you don't move the ownership around -- for most cases it compiles to the same exact thing as the manual counterpart. It's not completely free in all cases and introduces compile-time overhead and makes the language one step more complex, but in this case I'd say the benefits outweigh the downsides.
This isn't true. Simple example:
https://godbolt.org/z/pfO366
test1 and test2 are the "same", the transfer ownership to func1 and func2. Version2 requires you to keep track of it manually, version 1 uses unique_ptr. As you can see in the assembly, test1 has more assembly. It needs to check at the end of the function if the unique_ptr was moved by func1. As it cannot see the implementation it does not know if this happens, so it needs to check this.

Also because if this extra code, it cannot do tailcall optimization. If you enable exceptions, test1 has a bunch more instructions, but those only hit when you have an exception. (note, clang does slightly better then gcc, 1 instruction less, msvc does a lot worse)

Now, this might not translate into an actual performance loss if you are memory/cache bound. But it shows that there is not "no runtime overhead".
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
powly
Level 4
****



View Profile WWW
« Reply #65 on: October 17, 2019, 04:40:57 AM »

Didn’t bother to finish the sentence? Smiley The claim was somewhat untrue tho, later on I tried and it doesn’t generate the same asm (even if it’s very comparable, it’s different.)
Logged
qMopey
Level 6
*


View Profile WWW
« Reply #66 on: October 17, 2019, 10:29:55 AM »

C has no builtin OOP support, C++ does, OOP is overused and mis-applied often, so C++ is bad over C? Hm. I don't follow that.

Almost, but not quite. See this quote below.

I'm not saying "use C", I'm criticizing the dogma around modern C++. Using a more C-style subset of C++ is merely my solution to actively pressing against this kind of dogma.

More like C++ has more things to abuse, and once someone uses some of it, it's really hard to communicate to other people they shouldn't use all of it. It's a slippery slope of assumptions that is hard to fight against.
Logged
Daid
Level 3
***



View Profile
« Reply #67 on: October 17, 2019, 01:55:39 PM »

More like C++ has more things to abuse, and once someone uses some of it, it's really hard to communicate to other people they shouldn't use all of it. It's a slippery slope of assumptions that is hard to fight against.
By that logic, we shouldn't be using a lot of things. Like... Philips screws, or cars, or electricity. So many things to abuse there. Or coffee even, you really can abuse that. Or maybe even food in general, there are so many types of food, and so many to abuse, it's hard to communicate what they should and shouldn't eat. It's a slippery slope, you better not eat at all! Durr...?


Then again, C most definitely has the most libraries available. And, we just got told by the almighty qMopey that having many options and needing to learn which are good for you is a good reason to avoid all of it. So, avoid all C libraries! Blink Or maybe, avoid C, as it has many options to need to learn to prevent abuse.



As for the goal of this topic, I would love for C++ to deprecate that "0" may be implicitly cast to a pointer. This is one of those stupid C compatibility things that should just be killed. (gcc has a warning you can enable for this "-Wzero-as-null-pointer-constant", but sadly many people still depend on this stupid behavior)
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
fluffrabbit
Guest
« Reply #68 on: October 17, 2019, 07:01:35 PM »

I want trinary bools with `false`, `true`, and `very true`.
Logged
qMopey
Level 6
*


View Profile WWW
« Reply #69 on: October 17, 2019, 07:34:47 PM »

I'll take my exit now. The only reason I stayed so long is that Daid kept providing respectful responses. But now instead of continuing discussion, my points are merely mocked.
« Last Edit: October 17, 2019, 07:43:40 PM by qMopey » Logged
ThemsAllTook
Administrator
Level 10
******



View Profile WWW
« Reply #70 on: October 17, 2019, 08:24:43 PM »

Thanks for participating as long as you did, qMopey. I always appreciate your insights.
Logged

Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #71 on: October 17, 2019, 10:44:48 PM »

Just grabbing this quote to clarify something:
RAII for releasing memory (for example with a std::unique_ptr) is the direct "non-manual" counterpart for new/delete (so for simple types, malloc/free). It has no run-time overhead, at least if you don't move the ownership around -- for most cases it compiles to the same exact thing as the manual counterpart. It's not completely free in all cases and introduces compile-time overhead and makes the language one step more complex, but in this case I'd say the benefits outweigh the downsides.
This isn't true. Simple example:
https://godbolt.org/z/pfO366
test1 and test2 are the "same", the transfer ownership to func1 and func2. Version2 requires you to keep track of it manually, version 1 uses unique_ptr. As you can see in the assembly, test1 has more assembly. It needs to check at the end of the function if the unique_ptr was moved by func1. As it cannot see the implementation it does not know if this happens, so it needs to check this.

Also because if this extra code, it cannot do tailcall optimization. If you enable exceptions, test1 has a bunch more instructions, but those only hit when you have an exception. (note, clang does slightly better then gcc, 1 instruction less, msvc does a lot worse)

Now, this might not translate into an actual performance loss if you are memory/cache bound. But it shows that there is not "no runtime overhead".

So you're comparing a function that allocates and deletes vs. a function that allocs but doesn't delete? I fixed that for you: https://godbolt.org/z/Bed-LK

Explanation: at the first function call move construction of the unique_ptr happens. Because you only declared the func()s, compiler doesn't know what happens to the pointers, and has to insert the "delete" call just in case the unique_ptr comes out unmodified after the move. After all, std:move() doesn't move, it just flags the object as "feel free to take from it".

So the correct comparision would be for the C version to take the pointer by pointer and maybe in that call set the pointer to zero. In case it didn't set the ptr to zero, the caller needs to free() it. And to no-ones surprise the asm now looks identical.

You could make a case here that "in C I have to declare manually how resources are passed, and thus I can save a few cycles in complex scenarios." where in C++ it is "you can also use smart pointers and be safe against leaks if you want."

I guess your point was "those helpers are there, so now people use them even where I wouldn't". But you should at least be honest instead of trying to make up distorted comparisions.
Logged

Snake World, multiplayer worm eats stuff and grows DevLog
buto
Level 0
***



View Profile WWW
« Reply #72 on: October 18, 2019, 10:18:58 AM »

Quote
Thanks for participating as long as you did, qMopey. I always appreciate your insights.
Second that! Even/especially as a long time (and still happy) C++ user Smiley
Logged

Daid
Level 3
***



View Profile
« Reply #73 on: October 18, 2019, 10:54:14 AM »

So you're comparing a function that allocates and deletes vs. a function that allocs but doesn't delete?
No, as my post says, ownership is transferred to func1/func2. In the case of std::unique_ptr, ownership is transferred into the parameter with the move. And then when the parameter gets destroyed after the function call, std::unique_ptr checks if the ownership was transferred further or if it needs to be destroyed. Func1 does another move to actually transfer ownership.
The initial std::unique_ptr destructor gets optimized away, as it knows for a 100% sure that the data was transferred into the parameter and thus that it needs no work. (compare the unoptimized version, and then the code size explodes)

In the raw pointer case, ownership is transferred by documentation. The caller needs to know this happens. While it's a bit rare to transfer ownership into functions in C (it happens, but not often) It's quite common to get pointers back from functions that you own or not own. And thus manually keeping track of what you own and what you should free is common. std::unique_ptr can help here, but with a bit of runtime overhead. And that was my point, there is a minor runtime overhead.

I generally use std::shared_ptr or my own smart pointer, where objects own themselves, and handle their own lifetime. (On which I could dedicate a whole WTF topic on it's own)
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
Daid
Level 3
***



View Profile
« Reply #74 on: October 18, 2019, 11:06:54 AM »

I want trinary bools with `false`, `true`, and `very true`.
I encountered those in visual basic 6. (yes, I'm cursed, but this was well over 10 years ago)
That version (and older) a bool is simply a int32_t with the value 0 or -1. And it has no boolean logic operators, so "and" "not" "or" all work bitwise. Which is all fine if you have 0 and -1. But, if you get a "1" as true value back from some API, the you have a "very true", and "NOT very true" is still "very true", as binary not on 1 becomes -2. And the final "IF" statement just checked for 0 for false, any other number for true as you would expect.


On a more serious side, in digital logic, you have a lot more states, while most people think of digital logic as binary.
See: https://en.wikipedia.org/wiki/IEEE_1164
So I see your ternary bool, and I raise you as nonary bool!
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #75 on: October 18, 2019, 11:22:34 AM »

So you're comparing a function that allocates and deletes vs. a function that allocs but doesn't delete?
No, as my post says, ownership is transferred to func1/func2. In the case of std::unique_ptr, ownership is transferred into the parameter with the move. And then when the parameter gets destroyed after the function call, std::unique_ptr checks if the ownership was transferred further or if it needs to be destroyed. Func1 does another move to actually transfer ownership.
The initial std::unique_ptr destructor gets optimized away, as it knows for a 100% sure that the data was transferred into the parameter and thus that it needs no work. (compare the unoptimized version, and then the code size explodes)
Well... this is not the case, but I don't know how to explain it differently, so let's just leave it at that.
Logged

Snake World, multiplayer worm eats stuff and grows DevLog
Daid
Level 3
***



View Profile
« Reply #76 on: October 18, 2019, 01:37:33 PM »

So you're comparing a function that allocates and deletes vs. a function that allocs but doesn't delete?
No, as my post says, ownership is transferred to func1/func2. In the case of std::unique_ptr, ownership is transferred into the parameter with the move. And then when the parameter gets destroyed after the function call, std::unique_ptr checks if the ownership was transferred further or if it needs to be destroyed. Func1 does another move to actually transfer ownership.
The initial std::unique_ptr destructor gets optimized away, as it knows for a 100% sure that the data was transferred into the parameter and thus that it needs no work. (compare the unoptimized version, and then the code size explodes)
Well... this is not the case, but I don't know how to explain it differently, so let's just leave it at that.
https://godbolt.org/z/P07X31
If that's not the case, explain this. "obj" has to be empty after the call to func1. Doesn't matter if test1 moved it or not. It's ownership is moved as soon as the move happens in the parameter evaluation of the calling function (with gcc this happens in your own function, I don't know the MSVC ABI, which might handle things differently. I know it does for RVO, but don't know how it handles this)

If you take the disassembly of this code, you can see that the func1 implementation is really small, just a call to printf. So that is not doing the delete for sure.
Logged

Software engineer by trade. Game development by hobby.
The Tribute Of Legends Devlog Co-op zelda.
EmptyEpsilon Free Co-op multiplayer spaceship simulator
fluffrabbit
Guest
« Reply #77 on: October 18, 2019, 07:14:33 PM »

I want trinary bools with `false`, `true`, and `very true`.
I encountered those in visual basic 6. (yes, I'm cursed, but this was well over 10 years ago)
That version (and older) a bool is simply a int32_t with the value 0 or -1. And it has no boolean logic operators, so "and" "not" "or" all work bitwise. Which is all fine if you have 0 and -1. But, if you get a "1" as true value back from some API, the you have a "very true", and "NOT very true" is still "very true", as binary not on 1 becomes -2. And the final "IF" statement just checked for 0 for false, any other number for true as you would expect.


On a more serious side, in digital logic, you have a lot more states, while most people think of digital logic as binary.
See: https://en.wikipedia.org/wiki/IEEE_1164
So I see your ternary bool, and I raise you as nonary bool!
Wow, bitwise logic is no fun at all. I wouldn't worry about the goings on at the CMOS level, but stochastic logic may make sense in some places. As in, sometimes you have a weak inkling that code should do a certain thing, and sometimes you're really sure. Let the compiler fill in the blanks.
Logged
Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #78 on: October 19, 2019, 03:42:41 AM »

...
Not sure what you want to prove there. printf() is a huge function with lots of side effects, so I would prefer to not introduce it.

The other example, though: I thought that parameter evaluation happens in the callee. Which means that passing a std::unique_ptr by value into a function still means the compiler can't see what's happening in there if you only declare it. So it has to put the unique_ptr dtor to be safe.

From a human's point of view I agree with you: the compiler could know that passing a unique_ptr by value guarantees that it's Null afterwards. But if the parameter passing happens at the callee, the compiler doesn't know what happens there anymore, and therefore can't guarantee that the ptr is Null afterwards.

If you implement func1() and func2() to match their usage, the compiler knows what happens there, and suddenly it *can* tell that the ptr is guaranteed to be Null. Now both versions yield identical Asm. And this is why I deemed your comparision to be uneven.

See this https://godbolt.org/z/j4UPKC

Logged

Snake World, multiplayer worm eats stuff and grows DevLog
Omar_AA
TIGBaby
*



View Profile
« Reply #79 on: December 07, 2019, 06:44:28 PM »

Going with the thread topic:
Personally, I want static reflection and introspection at compile time. Right now I'm using a library called visit_struct that allows me to apply a lambda function over the members of a struct(recursively even); however, to make a struct visitable, you have to set it up using a macro. I kinda wish C++ provided that kind of introspection.
Logged
Pages: 1 2 3 [4]
Print
Jump to:  

Theme orange-lt created by panic