C++ tutorials

Started by
93 comments, last by taby 1 year, 6 months ago

JoeJ said:
Yes, but my claim is those are high goals which can't be enforced to work just by following OO paradigms, SOLID, or whatever.

Who has claimed the opposite in this thread?

JoeJ said:
There always are failures, and if so, the enforcement becomes an obstacle.

Please give me an example, because I have never encountered one, even in 10+ years of working on legacy C++ codebases. Again, what are you doing that being able to hack private members is somehow more important than the benefits private members get you in the first place?

JoeJ said:
So i'd like to break the private constraint, in cases i am willing to accepting the consequences, which include making my hacks hard to maintain and them being eventually just temporary solutions. I mean, C was meant to do anything. We can cast one type to another, even if surely incompatible. We can shoot our own foot if we desire.

Unless you're off on your own doing lone wolf hobby stuff, you work on a team, and your coworkers probably aren't willing to accept those consequences or maintain the hack. Certainly I would not be. Every time I have seen someone think they needed that, they actually didn't.

JoeJ said:
Why the hell should private / public be an exception from this tradition?

Because C++ is not C and has a different "tradition." C++'s tradition is zero-runtime cost abstractions + using the type system to prevent errors and enforce resource lifetimes. C and C++ should not be conflated at a cultural level, nor should you approach C++ with the mindset you do with C.

JoeJ said:
This is like saying, suddenly modern programmers must be protected from themselves, because they tend to do everything wrong and are just stupid.

You say this in the same post where you conflate OOP with deep inheritance hierarchies. ?

It's not that modern programmers are “stupid”, it's that unless there is a counter-pressure against it, they tend to do the smallest amount of work and write the least amount of code they can get away with to solve the exact problem they are facing right now. Frequently this is a good thing, because shipping code is good and overengineering is not; sometimes the programmer's metaphorical myopia leads to tech debt, bugs, and code that is difficult to change because some user has yanked its internals out into its interface.

And protecting a programmer from themselves isn't quite it, either - as I said, programmers work in teams. When a programmer hacks something and it breaks later, if they aren't working alone it's not them alone who will be hit by the consequences. If you're on a big enough team, it won't even just be the programmers; maybe something breaks in production and now your players are, too - and maybe it's bad enough even that your game's community managers are getting death threats. Even if it breaks before ship, there's still going to be time wasted by producers, QA testers, etc. in finding and recording and scheduling the bug… How many hours or even days a month from now will be wasted by a shortcut taken today?

There's even more to it, though. If you see that a type is specified to behave in a certain way, you will form certain expectations of how code that uses that type will behave that inform your understanding of what the code does and what could possibly go wrong. If there's one or two places where a type's design contract is subverted, the contract provided by the type system (and possibly API documentation) is no longer guaranteed and the fallout may impact the expectations that unrelated code has. Suddenly you can't trust that type to actually behave as specified anymore, when you're reading code that uses it. In this way, the team's cognitive burden has increased; they have to be more careful, read the code even more closely than usual, because you have introduced a possibility that the code does not do what it says it does. That negates a big part of the benefit of using the type system to enforce design contracts in the first place!

If you don't want to use the type system to enforce design contracts (which is your choice, though I do not endorse it), then you will not be comfortable with the direction C++ has evolved.

JoeJ said:
Well, i could add ‘#define private public’ to my preprocessor defintions

Actually you kinda can't, because that's undefined behaviour. The compiler is - if I recall correctly - allowed to order private members separately from public ones. I've never seen a compiler do it, so you might just get away with it, but that doesn't mean you should roll the dice on this. Whenever you invoke undefined behaviour and get away with it, you are at the mercy of the compiler author not to change things. I have had to fix far more bugs that resulted from UB than I have solved with UB, including some that were only uncovered by updating the compiler or targeting a newer C++ standard.

I've seen this come up with shipped video games and modding tools, too. One game I worked on had a scripting community post-launch that was exploiting a buffer overrun to get some data. They were getting lucky for years, because two arrays happened to be placed beside each other in memory and the one with the lower address was a constant size for those years. And then we went and add some new stuff to the game and that array grew in size… I had to write a shim in the function they were calling from script to fake the array being its original size, because “don't break userspace” was an important principle. All that could have been avoided if the original authors had just put in a bloody bounds check…

JoeJ said:
Exactly, but you can not always convince the author to do so.

Then you use different code code. Breaking the design contract in user code means the guarantees the author provided through the type system no longer apply. There may be very good reasons that the author will not do so. What makes your use case more important than whatever those reasons are?

Again, I'm interested in seeing an example where this would be justified.

JoeJ said:
But OOP does not give us that. We get no flexible graphs - we get restrictive and cumbersome hierarchies and trees.

Bad OOP, probably written in the '90s when people were new to it and thought they needed to use every OO language feature for code to be OO. Good OO doesn't have inflexible inheritance hierarchies. I'd argue that good OO doesn't even have inheritance hierarchies, usually; inheritance beyond a single level is probably a mistake in most cases.

Let's not blame a tool for the ignorance of its wielders. It might be that you, in your particular work environment, cannot trust your coworkers to apply OO tools in a helpful manner, but that is a your situation thing and if that's the case then you ought to acknowledge that, rather than generalize.

Advertisement

taby said:

I tried putting the lambda template code, but it tells me that template is not expected outside of the global scope. is there a simple sample code?

What C++ standard are you compiling with? My recollection is that templated lambdas is a C++20 feature.

Oberon_Command said:
Who has claimed the opposite in this thread?

No one. I guess game devs were among the first figuring out what ‘bad OOP’ is, remembering the failure on trying to achieve composition from class hierarchies.

Oberon_Command said:
Please give me an example, because I have never encountered one, even in 10+ years of working on legacy C++ codebases. Again, what are you doing that being able to hack private members is somehow more important than the benefits private members get you in the first place?

If you want to waste your time (but better not): http://newtondynamics.com/forum/viewtopic.php?f=9&t=9778

Trying to sum it it up instead: The physics engine i'm using is OOP from ground up. My world is a huge, single static mesh, no composition from small models as usual.
To support open world i need to to cut the mesh into chunks. To make the chunk boundary seamless with adjacent chunks, i have to add extra data to a chunk, in this case the face duplicated normals of adjacent faces which are in another chunk. And i need to point to those normals from the boundary edges.

This is actually a little change on data. No code should be affected. I want to keep the engines default functionality of building a BVH from the mesh, but i want to disable geometry processing such as merging coplanar faces and calculating adjacency pointers. My geometry pipeline already does those things and saves the results to disk, so no need the physics engine wastes performance on doing it again while streaming the data at runtime.

This should make sense, but it is something the developer did not expect. The only way to add static geometry to the engine is a constructor, which then does (redundant) geometry processing, build BVH, (redundant) searching to find adjacency.
That's not a OOP flaw. Imo he just lacks the interfaces so people which have their own geometry pipeline can do only the tasks they need to be done. That's not many of his users i guess, so i could not convince him to add those interfaces.
To fix it, i have to modify his code, by adding one more parameter to his constructor to disable the redundant stuff. No big issue, but still one more change i need to do now every time i do an update.
The OOP flaw then shows when i have to add my extra normals, and my preprocessed adjacency data, because the data structures are private.
Also, it took me 2-3 days to figure all this out. Due to abstraction and inheritance it was just difficult to figure out all the necessary engine internal details.

It's not the end of the world, and i'm quite happy i could achieve exactly what i wanted without any real changes on the data structures or functionality.
Still, although i only need to change data, OOP makes it cumbersome, unnecessarily difficult, and even ‘bad practice’ to get there.
It should not be difficult to change data, and there should be no language features to enforce barriers even to people who know what they do.
There should be only language features making those people aware that their low level access might break something like a contract, assumptions on data, etc., and that such data manipulation isn't guaranteed to keep working on future changes, eventually.

I could make much more such examples, also regarding using my own code. If issues or restrictions show up, i can just change it. No problem. But then it was a problem before the changes. And more often than not, the problem wasn't about data structures or functionality, but about OOP designs to access those things.

Oberon_Command said:
Actually you kinda can't, because that's undefined behaviour.

Hehe, thanks for destroying my last hope on a plan B, in case anything else fails. : )
Though, ofc. i did not seriously consider this.

Oberon_Command said:
Good OO doesn't have inflexible inheritance hierarchies. I'd argue that good OO doesn't even have inheritance hierarchies, usually; inheritance beyond a single level is probably a mistake in most cases.

Agreed. And i'm no master of good OOP for sure. I found other ways of trying to keep my stuff modular, reusable and maintainable. I think that's our job and problem, and language features or paradigms can't help us so much.

Coming back to your first question, there sure is no need t fight some majority of OOP evangelists here, or anywhere else. I don't have much of a problem with OOP, but it's always fun to rant. >: )

@Oberon_Command Yes, sir. I tried using the /std:c++20 command line parameter.

This is the code that I'm trying:

// https://www.programiz.com/cpp-programming/functors
// https://www.cppstories.com/2020/05/lambdasadvantages.html/
// https://en.cppreference.com/w/cpp/language/lambda

#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class A
{
public:

	void print(void)
	{
		cout << "A" << endl;
	}
};

int main(void)
{
	auto x = []<typename T>(const A<T> t)
	{
		t.print();
	};

	A<float> a;

	// error C2275: 'A<float>': 
	// expected an expression instead
	// of a type
	x<A<float>>(a);

	return 0;
}

taby said:
This is the code that I'm trying:

I see a number of problems here. First of all, print() is a non-const method and you're calling it on an argument that is const. So that's one compiler error you should be seeing. Second of all, look at the function you wrote. The type that your lambda needs to specialize on - the “T” - is not A<float>, but simply float in this context, because the argument type is already specified to be A<T>. If you try to explicitly specialize it to A<float>, then what you'll get is a lambda that expects an A<A<float>>, which is not what the “a” that you're trying to pass in is.

Finally and more importantly, x is not a function in this context- it's a variable, specifically an instance of the anonymous type created by the lambda, so trying to specialize it explicitly like that is not going to work. Remember, lambdas do not actually create functions. They create an anonymous (and unnameable) structure type that overloads operator(). What you would actually need to specialize is that anonymous type's operator(), but I would expect the compiler to do that for you in this context. You do not need to explicitly specify the type here unless the compiler cannot figure it out on its own:

	auto x = []<typename T>(const A<T> t)
	{
		t.print();
	};

	A<float> a;
	x(a);

If you really need to be explicit about what T is for some reason, then you need to also call the operator() explicitly:

	auto x = []<typename T>(const A<T> t)
	{
		t.print();
	};

	A<float> a;
	x.operator()<float>(a);

Thank you sir, it all works now!

Oberon_Command said:

taby said:
This is the code that I'm trying:

I see a number of problems here. First of all, print() is a non-const method and you're calling it on an argument that is const. So that's one compiler error you should be seeing. Second of all, look at the function you wrote. The type that your lambda needs to specialize on - the “T” - is not A<float>, but simply float in this context, because the argument type is already specified to be A<T>. If you try to explicitly specialize it to A<float>, then what you'll get is a lambda that expects an A<A<float>>, which is not what the “a” that you're trying to pass in is.

Finally and more importantly, x is not a function in this context- it's a variable, specifically an instance of the anonymous type created by the lambda, so trying to specialize it explicitly like that is not going to work. Remember, lambdas do not actually create functions. They create an anonymous (and unnameable) structure type that overloads operator(). What you would actually need to specialize is that anonymous type's operator(), but I would expect the compiler to do that for you in this context. You do not need to explicitly specify the type here unless the compiler cannot figure it out on its own:

	auto x = []<typename T>(const A<T> t)
	{
		t.print();
	};

	A<float> a;
	x(a);

If you really need to be explicit about what T is for some reason, then you need to also call the operator() explicitly:

	auto x = []<typename T>(const A<T> t)
	{
		t.print();
	};

	A<float> a;
	x.operator()<float>(a);

Since you are compiling with C++20 anyway the compiler can deduce your template arguments so writing just x(a) will call the right operator, there is no need to explicitely call the call operator in that case. The point of having the call operator overloaded is that the object can act like a function anway.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

Thanks everyone. I really appreciate it!

Gonna do up a code for using <utility>.

Any other recommendations are greatly appreciated.

I added a 24th sample code, this time covering std::sort() and how it beats most sorting algorithms, such as the counting sort.

https://github.com/sjhalayka/sample_cpp_code/blob/main/03_advanced/s24_sorting.cpp

This topic is closed to new replies.

Advertisement