Criticism of C++

Started by
143 comments, last by Ryan_001 8 years, 3 months ago

The compiler could arguably optimize a check that happens after a division (similar to removing nullpointer checks after a pointer has already been dereferenced), but code that only runs checks like this after the fact is utterly broken -- so if the compiler is indeed smart enough to notice that this is happening, it should produce an error (or at the very least a warning). It shouldn't silently optimize. Never, not ever. This is something that the programmer absolutely wants to know.

So, from that point of view, these "optimization opportunities" can be seen as wrong compiler behavior.


This is actually one of the reasons "volatile" exists.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Advertisement

I skimmed through the discussion and didn't see anyone post this here but Jonathan Blow has talked a lot about his issues with C++ and actually implemented his own language. He shows why his language is more well-suited for games than C++ is. He started off with lectures and then went to demos. You can find the first lectures here (the demos are on his channel)

To give a quick summary of his points about why he doesn't like C++:

1) A language should understand that the shipping code is not the way the code will look like through development. He says that certain code modifications should be fast and seamless so that we have less busy/braindead work. A few examples of these are the fact that accessing a member of a class by value uses the "." operator while accessing via pointer uses the "->" operator. In reality, both are exactly the same but as you are writing your code and your switching between accessing a struct by value or by reference, you constantly have to modify these operators even though the logic is the same (could kill a lot of time depending on how many times you switch and how many members are part of the struct).

Another example was the process of taking code that starts to repeat itself and putting it into a lambda function and then into a proper function. The idea here is that if I have a function and I notice that I'm repeating some lines of code, then maybe its useful to have a lambda function which defines that process and then call it where I need it in that one function. I don't want to make it a proper function just yet because I'm only using it in one function so when someone comes along to analyze the code, they don't have to assume that the given function can be called from anywhere. Keeping it lambda keeps it more local. Once I actually need that functionality somewhere else, than I want to easily move my lambda function out into a proper function. In C++, this requires a ton of work since due to how lambdas are defined. And then going from lambda to a proper function requires more busy work since now the layout of everything is changed again. Jonathan's approach is like pythons where you just have nested functions that have the exact same syntax as normal functions. This makes the transformation of code from one state to another as you are in the process of writing it much easier since you only copy paste the function to some other location unlike C++ where the way I define functions and lambdas is completely different.

2) Another point he makes is since this language should be for games as a priority, it would be nice to support common things game developers due for optimization. For example, if I have a struct for a model with a pointer to the vertices and pointer to the indices, then its a lot faster to allocate one block of memory for these 2 pointers and then just say that the second pointer points to where the vertices data ends (save a allocation). In C++, I have to do a lot of array indexing or pointer arithmetic to do that but he makes a keyword which tells the language that the memory 2 pointers point to should be one block of memory. Then when we deallocate the memory for the struct, we deallocate the entire block and the language knows the size and all that. This is a really specific feature but he notices that its something he and his friends use a lot so giving support for it and making it one keyword vs like 15 lines of code that you feel unsafe about is a much better option.

3) Instead of using templates or macros for preprocessing, he argues why not just run the language code directly at preprocess time. You use a keyword that runs a given function at preprocess time, computes whatever it returns, and sticks it into the line which it is found in. Instead of playing around with ugly template syntax and messing around with a different language like macros, we just run the language we are using directly but during compilation.

4) Jonathan argues to remove header files and just have one type of file like in java. The idea here is to remove the busy work of defining a function in one file, then jumping to another file to to write its implementation. He also wants to get rid of the #pragma once keywords and the order of function definitions and just have the compiler sort out dependencies itself like in many high level languages (again, getting rid of needless busy work).

... And more.

He talks about a lot more stuff and the best part is all of these features and many more have already been implemented by him and hes constantly refining it, seeing what works and what doesn't. The stuff I talked about is mostly from his 2 lecture videos but he does a whole lot more in the demo videos and its pretty amazing stuff. In my opinion, his language is a really good alternative to C++ for gamers since it focuses on getting rid of lots of busy work and spending more time coding in easy to understand syntax so that you don't feel uneasy about what you just wrote (and it seems to succeed in its goals).

1) A language should understand that the shipping code is not the way the code will look like through development. He says that certain code modifications should be fast and seamless so that we have less busy/braindead work. A few examples of these are the fact that accessing a member of a class by value uses the "." operator while accessing via pointer uses the "->" operator. In reality, both are exactly the same but as you are writing your code and your switching between accessing a struct by value or by reference, you constantly have to modify these operators even though the logic is the same (could kill a lot of time depending on how many times you switch and how many members are part of the struct).


std::unique_ptr<std::string> uPtrToString = ...;

uPtrToString.reset(); //Is this accessing std::string::reset(), or std::unique_ptr<>::reset()?
auto meow = uPtrToString[27]; //Is this accessing std::unique_ptr<>::operator[], or std::string::operator[]?

There's answers to these, and the compiler can do alot of the work, but nevertheless, the answers can't be decided at a whim and need serious thought, or the resulting language would be even worse than C++, or else have to make breaking changes to itself after a few years, like many non-C++ languages have had to do. smile.png

Are you going to allow operator . (dot) overloading? If not, you're saying you're going to ban convenient syntax for things like smart pointers and iterators? If you do allow dot operator overloading, it raises a different set of complications.

Another example was the process of taking code that starts to repeat itself and putting it into a lambda function and then into a proper function. The idea here is that if I have a function and I notice that I'm repeating some lines of code, then maybe its useful to have a lambda function which defines that process and then call it where I need it in that one function. I don't want to make it a proper function just yet because I'm only using it in one function so when someone comes along to analyze the code, they don't have to assume that the given function can be called from anywhere. Keeping it lambda keeps it more local. Once I actually need that functionality somewhere else, than I want to easily move my lambda function out into a proper function. In C++, this requires a ton of work since due to how lambdas are defined. And then going from lambda to a proper function requires more busy work since now the layout of everything is changed again. Jonathan's approach is like pythons where you just have nested functions that have the exact same syntax as normal functions. This makes the transformation of code from one state to another as you are in the process of writing it much easier since you only copy paste the function to some other location unlike C++ where the way I define functions and lambdas is completely different.


They're actually not all that different:


                    [](const std::string &nya) { return nya; } //Return type auto-detected.
std::string MyFunction(const std::string &nya) { return nya; }
^--------------------^

Converting between them is basically a single copy+paste (two, if you're explicitly specifying the lambda's return type), and writing the function's name. tongue.png

My programming limitation is not how fast I type, it's how fast I think. Typing is not a bottleneck for me, optimizing C++ for faster writing isn't something I benefit from.

Optimizing for ease of reading/quick-scanning, that I benefit from.

Oh by the way, just incase you say "but lambdas *can* have explicitly specified return types!"
A) I've personally only had to use them once (and I use lambdas alot), so I wonder how common it actually is.
B) Regular functions can also use that syntax, so stick with that unified syntax throughout your project if you want that consistency.


             [](const std::string &nya) -> std::string { return nya; }
auto MyFunction(const std::string &nya) -> std::string { return nya; } //Yes, 'tis valid syntax. =)
^-------------^

But this is dancing around the real issue: These kind of refactoring transformations are well suited for tool features, like in your IDE. wink.png

Some features are best as core language features, some are best as library features, some are best as compiler features, and some are best for tool features. Sometime it takes a moment to realize the proper location for a new feature to go.

2) Another point he makes is since this language should be for games as a priority,

As you know, C++ isn't only for games. If every industry got to put their favorite features into the language, people would be complaining about that instead. tongue.png

it would be nice to support common things game developers due for optimization. For example, if I have a struct for a model with a pointer to the vertices and pointer to the indices, then its a lot faster to allocate one block of memory for these 2 pointers and then just say that the second pointer points to where the vertices data ends (save a allocation). In C++, I have to do a lot of array indexing or pointer arithmetic to do that but he makes a keyword which tells the language that the memory 2 pointers point to should be one block of memory. Then when we deallocate the memory for the struct, we deallocate the entire block and the language knows the size and all that. This is a really specific feature but he notices that its something he and his friends use a lot so giving support for it and making it one keyword vs like 15 lines of code that you feel unsafe about is a much better option.


That's not too difficult to do in C++, and you could probably create a template to do it for you at compile-time, though it would be messy. It would be nice feature to have though. Personally, I'm more eager to have first-class struct-of-array convenience syntax, because that's even more useful, and an ugly hacky messy pain to do in C++ currently. sad.png

Also, reflection support. But luckily C++ is getting reflection support in 2020, if not 2017.

3) Instead of using templates or macros for preprocessing, he argues why not just run the language code directly at preprocess time. You use a keyword that runs a given function at preprocess time, computes whatever it returns, and sticks it into the line which it is found in. Instead of playing around with ugly template syntax and messing around with a different language like macros, we just run the language we are using directly but during compilation.

Though it has a few things it needs ironed out, overall I like the template syntax, so that's a subjective comment. smile.png

But anyway, you are using two different terms. Are you talking about preprocessing, which templates don't do, or compile-time computation?

It sounds like you are talking about compile-time computation.

If so, C++ added that back in 2011, using the 'constexpr' keyword. Initially it was (intentionally) limited in C++11, but they expanded it more in C++14, and intend to keep on expanding it to do exactly what you're talking about: Running arbitrary C++ code at compile-time to compute things, within limitation. Currently it's not fully unleashed, as they are gradually relaxing restrictions with each C++ release, to make sure any problems are ironed out and to give compiler developers a chance to catch up, and so programmers has a consistent featureset until it's fully present.

Though, it seems you didn't take it far enough. Why need a special keyword? The compiler should just detect whether a chunk of code can be executed wholly or partially at compile time, and do it automatically. The D Language does this, supposedly to great effect. There was a whole C++ talk on this by the author of D, advocating for C++ to eventually develop in that direction.

4) Jonathan argues to remove header files and just have one type of file like in java. The idea here is to remove the busy work of defining a function in one file, then jumping to another file to to write its implementation.

He also wants to get rid of the #pragma once keywords and the order of function definitions and just have the compiler sort out dependencies itself like in many high level languages (again, getting rid of needless busy work).

Again, the jumping to another file is solved by better tools. I can Ctrl+Click in the IDE I use, to jump between declaration and definition. Whereas there are arguable benefits for separating the two. Many C++ programmers argue the opposite extreme of what you are saying, complaining that a C++ class's private member variables are visible in the header. ohmy.png

Now, headers have other problems (mostly as a result of macroes, which are the reason many seemingly unrelated areas of C++ have taken so long to get fixed), but C++ is migrating towards a module system similar to Python's, which will solve alot of issues.

Last I heard, the modules are supposed to get unofficially added to compilers in late 2017 or earlier 2018, and be officially standardized in 2020.

He talks about a lot more stuff and the best part is all of these features and many more have already been implemented by him and hes constantly refining it, seeing what works and what doesn't. The stuff I talked about is mostly from his 2 lecture videos but he does a whole lot more in the demo videos and its pretty amazing stuff. In my opinion, his language is a really good alternative to C++ for gamers since it focuses on getting rid of lots of busy work and spending more time coding in easy to understand syntax so that you don't feel uneasy about what you just wrote (and it seems to succeed in its goals).

Many other languages (again, like the D programming language) have some or more of the features you mentioned. (I haven't watched that particular video, but I've seen other videos from him describing his language).

Many people have good ideas for languages. Then they all run off and write them, and nobody uses them, because: They don't run on iOS. They don't run on Android. They don't run on any of the gaming consoles.

C++ definitely needs alot of work, but it's amazing the rapid progress that has been occurring since 2009 that we've already received, the things that are coming down the road, that we already know about. Partially

Saying, "yea, but you can use Bob's custom homebrewed language X right now! (if you don't want it to run on any gaming console, and if you want your code to be obsolete within a few months)" is only looking at half the picture.

Do you know there are at least three different GameDev.net members who are writing their own programming languages and are pretty far along that process? Of the top of my head, there's Epoch, AngelScript (mature and used by commercial releases), and ... crap, somebody is going to kill me for forgetting their project, but I know there is a third one. biggrin.png

How many hundreds of custom languages are getting built every year? And how many hundreds die every year? I don't want to build a business and find out in three years all my games are built on an unsupported language that will never be finished, or never get bug fixes, or never get ported. A language has to reach critical mass before it becomes viable. Out of the thousand or so languages to have existed, only about twenty general-purpose (non-DSL) languages have hit that and continue to maintain it (pulling the number 'twenty' out of thin air).

Great ideas exist, but there are reasons we can't just start using them. Partially this can be resolved by having these languages compile into C or C++, like Python does (and like C++ originally did), but that only solves some of the problems. mellow.png


C++ has different goals and try to keep them by forcing backward compability, what if C++ had to be designed nowdays from scratch? It will result in much more clean, simple and performant language with less maintenance burden both on client and compiler side.

And it would never have gained the popularity it has today. Without backwards compatibility, you don't get this huge code base that already compiles as C++ and can start using C++ features where you see them fit. If it had broken with backwards compatibility early on, it would be in a similar position that D is in today: People know about it, many think it's even a good language and still very few people (in comparison) use it.

The compiler could arguably optimize a check that happens after a division (similar to removing nullpointer checks after a pointer has already been dereferenced), but code that only runs checks like this after the fact is utterly broken -- so if the compiler is indeed smart enough to notice that this is happening, it should produce an error (or at the very least a warning). It shouldn't silently optimize. Never, not ever. This is something that the programmer absolutely wants to know.

So, from that point of view, these "optimization opportunities" can be seen as wrong compiler behavior.

This is actually one of the reasons "volatile" exists.
Care to explain how using volatile would help in any way to detect such a logic error? All that volatile does is disable useful optimizations (in fact most, if not all of them). I'm not talking about disabling most optimizations, I'm talking about not doing false optimizations, in other words dead-stripping demonstrably incorrect code.

Yes, from a purely syntactic and semantic (as far as the language goes) point of view, code that dereferences a pointer or does a division, and later checks that the pointer or variable isn't null/zero is perfectly correct. But it is nonetheless semantically incorrect from the point of view of program logic or programmer intent, or whatever you want to call it. The check is intended to prevent something that, if it occurs, has already happened at that time. That is the reason why the compiler optimizes that way, too. If the condition was possible at all, then undefined behavior would already have occurred earlier, so it's not possible that this can be, thus the code is eliminated.

Now, the point is, the compiler correctly detects the condition (apparently that's not a problem), but then instead of hinting the programmer to it, it does an entirely useless, stupid "optimization". Because the totally ill-behaving program might save one or two clock cycles, yay.

It's much more important for a program to work correctly, and as intended, not shave off a few cycles. What's "correct" can admittedly be hard to tell, but in this case the compiler has demonstrably detected that the program is incorrect (otherwise it couldn't perform that "optimization").

Heck, I wouldn't even care if that optimization happens, as long as it doesn't happen silently.

Note that i know almost nothing about Jonathans language except for one video i've seen a few years back when he first started working on it.


std::unique_ptr<std::string> uPtrToString = ...;

uPtrToString.reset(); //Is this accessing std::string::reset(), or std::unique_ptr<>::reset()?
auto meow = uPtrToString[27]; //Is this accessing std::unique_ptr<>::operator[], or std::string::operator[]? 
Are you going to allow operator . (dot) overloading? If not, you're saying you're going to ban convenient syntax for things like smart pointers and iterators? If you do allow dot operator overloading, it raises a different set of complications.

This only assumes the language itself doesn't provide you with mechanisms for pointer ownership. I recall from when i watched his first video that he wants to have a mechanism where the language and compiler themselves know that a variable is the owner of a pointer. Imagine it as having the unique_ptr as a built-in type, and not a template as well.
As far as dot operator overloading, that is a valid question, one which is probably answered in one of his videos, maybe.


                    [](const std::string &nya) { return nya; } //Return type auto-detected.
std::string MyFunction(const std::string &nya) { return nya; }
^--------------------^
Converting between them is basically a single copy+paste (two, if you're explicitly specifying the lambda's return type), and writing the function's name. tongue.png

How do you easily and cleanly move lambdas that have captures? No idea if his language can do the same, so not sure the question is valid enough.

3) Instead of using templates or macros for preprocessing, he argues why not just run the language code directly at preprocess time. You use a keyword that runs a given function at preprocess time, computes whatever it returns, and sticks it into the line which it is found in. Instead of playing around with ugly template syntax and messing around with a different language like macros, we just run the language we are using directly but during compilation.


But anyway, you are using two different terms. Are you talking about preprocessing, which templates don't do, or compile-time computation?
It sounds like you are talking about compile-time computation.

If so, C++ added that back in 2011, using the 'constexpr' keyword. Initially it was (intentionally) limited in C++11, but they expanded it more in C++14, and intend to keep on expanding it to do exactly what you're talking about: Running arbitrary C++ code at compile-time to compute things, within limitation. Currently it's not fully unleashed, as they are gradually relaxing restrictions with each C++ release, to make sure any problems are ironed out and to give compiler developers a chance to catch up, and so programmers has a consistent featureset until it's fully present.

But the end result is the same, no? You get some automatic code generation done for you.
In C, you have only macros to do things templates can do in C++. Granted, they both have their uses in C++, but it would help if you stopped looking at Johns language as an upgrade over C++, rather as an upgrade over C. As was mentioned in this thread, there's a smaller, simpler an easier language in C++ trying to get out, and that language is C. So why not improve C with some of the stuff C++ does better?
Also, constexpr is very limited right now, and will probably stay limited to some degree. Johns language has no limitation. Sure, you'd have to control yourself and know that if you invoke an expensive function during compilation, it's gonna slow it down. But then again, you're a programmer, you're supposed to know the tradeoffs of doing so.

4) Jonathan argues to remove header files and just have one type of file like in java. The idea here is to remove the busy work of defining a function in one file, then jumping to another file to to write its implementation.

He also wants to get rid of the #pragma once keywords and the order of function definitions and just have the compiler sort out dependencies itself like in many high level languages (again, getting rid of needless busy work).

Again, the jumping to another file is solved by better tools. I can Ctrl+Click in the IDE I use, to jump between declaration and definition. Whereas there are arguable benefits for separating the two. Many C++ programmers argue the opposite extreme of what you are saying, complaining that a C++ class's private member variables are visible in the header. ohmy.png

Now, headers have other problems (mostly as a result of macroes, which are the reason many seemingly unrelated areas of C++ have taken so long to get fixed), but C++ is migrating towards a module system similar to Python's, which will solve alot of issues.
Last I heard, the modules are supposed to get unofficially added to compilers in late 2017 or earlier 2018, and be officially standardized in 2020.

Sure you can jump easily with your IDE, but you'd have to jump less if we'd all just have the .cpp files instead of .h files, no? This might be the thing modules fix after they roll out, but i believe he was referring to having a java/C# model of just having one file type and the compiler figures out all the function names and symbols without you having to write a separate header file to include all over the place just so you can call a function cross files.

C++ is a general purpose language, and has it's flaws when it comes to certain domains, this much we can agree on, and it's been mentioned throughout this thread. As such it will never be perfect for game development. So why shouldn't we strive to have a language specifically suited for game development? Even if it's just C with some extra blows and whistles, some borrowed from C++, some invented to suit the needs of game development. Dismissing attempts to do so because we have something good enough... Well, the wheel got reinvented from time to time, didn't it?

images%2B%25283%2529.jpg

devstropo.blogspot.com - Random stuff about my gamedev hobby


and ... crap, somebody is going to kill me for forgetting their project, but I know there is a third one.

Telastyn's Tangent, perchance?

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

And lets not forget the possibility that the compiler actually decides to evaluate the parameters simultaneously, squeezing in calculations to either take advantage of hyperthreading or interleave with long memory load times.

And why could it not do that? Defining the order of evaluation doesn't preclude it from behind the scene's optimizations. All code, which has to be run sequentially, can still be reordered in the final assembly. The only time this doesn't happen is when there are dependencies, in which case you don't want it to re-order them anyways.


If you force a certain order for parameter evaluation you have now introduced dependencies. You are forcing the compiler to finish the work for parameter X, before it starts work on parameter Y. By saying that parameter evaluation is unordered you remove those dependencies and allow the compiler to re-order the code to produce the most optimal result.

The compiler could arguably optimize a check that happens after a division (similar to removing nullpointer checks after a pointer has already been dereferenced), but code that only runs checks like this after the fact is utterly broken -- so if the compiler is indeed smart enough to notice that this is happening, it should produce an error (or at the very least a warning). It shouldn't silently optimize. Never, not ever. This is something that the programmer absolutely wants to know.

So, from that point of view, these "optimization opportunities" can be seen as wrong compiler behavior.


This is actually one of the reasons "volatile" exists.


No, volatile exists for things like device drivers where pointers aren't really pointers to memory addresses, but they are special memory-mapped IO where setting or reading a value changes the state of some hardware such that the compiler cannot make any assumptions about the state of that memory address before or after an operation takes place.

For example, if you do "myVolatile = 1" the compiler can't even assume that myVolatile is equal to 1 on the very next line, because setting it to 1 may have triggered hardware that now sets the value to 0, the return value of the operation.
I'm afraid I found most of Mr Blow's justifications for his new language to be based on very strawman arguments. For example, when justifying his new ownership semantics, he says something like "Of course, you could use std::vector but nobody I know does that." Maybe his argument applies more against C, where templates are not available and ugly macro hackery or unsafe void pointers are the only way to write generic containers.

I take the point from the above poster about allocating two buffers in a single block of memory, but be fair, it isn't at all difficult to do this in C++ either.

I write languages for fun and there is always the temptation to think that your own could be useful to others, but its about so much more than a good language, as has been discussed. Its about existing codebase, existing developer skills and training, amount of learning resource available, availability of compilers etc.

I'm personally very happy working in C++11 and C++17 looks like its going to be cool too. I like the philosophy of don't implement something in the language that can be implemented in a third party language.

I'm well aware C++ has its issues, don't get me wrong, but as a familiar user, they very rarely cause me any problems.

If you force a certain order for parameter evaluation you have now introduced dependencies. You are forcing the compiler to finish the work for parameter X, before it starts work on parameter Y. By saying that parameter evaluation is unordered you remove those dependencies and allow the compiler to re-order the code to produce the most optimal result.


Not true. Even in a simple function like:


int F(int a) {
   int b = a + 1;
   int c = a + 2;
   int d = b + c;
   return d;
   }


According to the 'language' b must be evaluated before c (due to sequence points). But in the underlying assembly code generated, the operation a+1 may not come before a+2. Its even possible that b and c are optimized right out and all you have left is the assembly equivalent of d = 2*a + 3 (probably through a single lea instruction). It can do these optimizations because of dependency analysis. So lets consider a similar example:


// same F() as above

void G(int a) {
   return a*5;
   }

int H(int a, int b) {
   return a + b;
   }

// ....

int x = H(F(5), G(6));

If we state in the language that F() must evaluate before G(), that doesn't mean that's what the compiler will emit in the assembly. It can still do whatever optimization it wants provided the same outcome as if F() was evaluated before G(). As far as I can tell, for any scenario where F() and G() are not dependent, the emitted assembly will be identical regardless of whether there is a fixed order of evaluation or not. The only time the emitted code would differ is when there are dependencies. So what do we do then? As its stands now we manually spell out the order of evaluation with something like:


int f = F(5);
int g = G(6);
int x = H(f, g);

In the vast majority of cases though we just want left to right evaluation (it just makes sense, and is the way that other languages also use, like D). Stating left to right evaluation as opposed to undefined or 'implementation defined doesn't reduce to potential for optimizations, its just cleans things up a bit (much like we no longer use the auto keyword for register allocation).

This topic is closed to new replies.

Advertisement