Simplifying code II

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Simplifying code II

Post by mvanthoor »

R. Tomasi wrote: Wed Sep 22, 2021 6:59 pm I totally agree. That's an outdated practice imho, too. There might have been performance reasons for doing so back in the days, but I hardly believe that is an issue at all with modern compilers. And checking flags abundantly doesn't help with performance, either, I would argue. There was a limitation of that kind for constexpr functions in older C++ standards, but that's gone, too.

EDIT: I just googled what the original reason for this was. Seems like "one return statement" never was actually meant. It was all about "one return adress" - which is quite a different thing:
https://softwareengineering.stackexchan ... -come-from
There is one other thing I just remembered, but it is only relevant in languages with fully manual resource control:

Code: Select all

function stuff() {
	x = malloc_something();
	y = 0;
	
	while !interrupted() {
		y = do_stuff();
		
		if ( SOME_ERROR ) then return NULL;	// You leak "x" here
	}
	
	free(x);
	return y;
}
In this function, you'd need two free() statements for one malloc(). It's very easy to forget the free() in the while loop. In garbage collected languages, or even Rust which kills all the locally allocated resources as soon as the function goes out of scope, this is a massive source of memory leaks in C. This might be one of the reasons of only one return, so you can write a function skeleton like this:

Code: Select all

function stuff() {
	x = malloc()
	y = 0;
	error = false;
	
	... do some stuff to calculate y ...
	... save an error var if necessary ...
	
	free(x);
	error ? return null : return y;
}
This way you won't have a memory leak, but you will have to catch, save and check lots of intermediate flags if the function does lots of complicated work on y. Maybe this is the reason why "one return" was a thing back when, but in that case, the reason became lost in time, with people just stating "a function should have only one return statement."
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
R. Tomasi
Posts: 307
Joined: Wed Sep 01, 2021 4:08 pm
Location: Germany
Full name: Roland Tomasi

Re: Simplifying code II

Post by R. Tomasi »

mvanthoor wrote: Sat Sep 25, 2021 10:46 am
[snip]

In this function, you'd need two free() statements for one malloc(). It's very easy to forget the free() in the while loop.
[snip]
Yeah, that's also one of the arguments made in the comments of the thread that I linked to. I admit that danger exists, but for me that is "kind of" an outdated argument aswell, because it is very language specific. In C++ (or any other object oriented language, I guess) the "right way" to tackle that danger is sticking to the RAII paradigm (resource allocation is instantiation). That way everything will be freed in the destructor, even if an exception is thrown (just having one return statement does not defend against that).
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Simplifying code II

Post by mvanthoor »

R. Tomasi wrote: Sat Sep 25, 2021 11:15 am Yeah, that's also one of the arguments made in the comments of the thread that I linked to. I admit that danger exists, but for me that is "kind of" an outdated argument aswell, because it is very language specific. In C++ (or any other object oriented language, I guess) the "right way" to tackle that danger is sticking to the RAII paradigm (resource allocation is instantiation). That way everything will be freed in the destructor, even if an exception is thrown (just having one return statement does not defend against that).
In Rust, RAII is mandatory. It's not an object oriented language (although it can do lots of object oriented things, apart from inheritance, which is left out on purpose), but it is mandatory to initialize and instantiate something when you declare it. If this is a local resource (like opening a file, or allocating a vec), Rust will close the file and de-allocate the vec if the function goes out of scope. It doesn't matter _how_ it goes out of scope: return, error, crash... Rust will do an "unwind" of all resources.

In the same vein, it's very hard (but not impossible) to call a function, initialize and instantiate a resource there, and then return a pointer to that resource. It's standard practice in C, but in Rust, it breaks the paradigm that you create and clean up stuff in the same place. There are ways around this (lifetimes, so you prove to the compiler that the object in that other function WILL live long enough to be used by another function, and that the other function WILL clean it up), but that's far from easy. This can become very complicated very fast, because you're doing the compiler's job.

That's the reason why I never did it in Rustic; I just avoid the "create a resource in function X and return a pointer to the resource" way of doing things. It works in C, if you are meticulous enough to keep track of those pointers yourself, but in Rust, it breaks the compiler's guarantees and you'll have to provide them yourself. If you're going to do that, it's MUCH easier to just use C.

Rule number one when using Rust: don't try to use the language as if it's C, or you'll fight the compiler every step of the way, and your code will become VERY messy.
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
R. Tomasi
Posts: 307
Joined: Wed Sep 01, 2021 4:08 pm
Location: Germany
Full name: Roland Tomasi

Re: Simplifying code II

Post by R. Tomasi »

mvanthoor wrote: Sat Sep 25, 2021 11:26 am
R. Tomasi wrote: Sat Sep 25, 2021 11:15 am Yeah, that's also one of the arguments made in the comments of the thread that I linked to. I admit that danger exists, but for me that is "kind of" an outdated argument aswell, because it is very language specific. In C++ (or any other object oriented language, I guess) the "right way" to tackle that danger is sticking to the RAII paradigm (resource allocation is instantiation). That way everything will be freed in the destructor, even if an exception is thrown (just having one return statement does not defend against that).
In Rust, RAII is mandatory. It's not an object oriented language (although it can do lots of object oriented things, apart from inheritance, which is left out on purpose), but it is mandatory to initialize and instantiate something when you declare it. If this is a local resource (like opening a file, or allocating a vec), Rust will close the file and de-allocate the vec if the function goes out of scope. It doesn't matter _how_ it goes out of scope: return, error, crash... Rust will do an "unwind" of all resources.

In the same vein, it's very hard (but not impossible) to call a function, initialize and instantiate a resource there, and then return a pointer to that resource. It's standard practice in C, but in Rust, it breaks the paradigm that you create and clean up stuff in the same place. There are ways around this (lifetimes, so you prove to the compiler that the object in that other function WILL live long enough to be used by another function, and that the other function WILL clean it up), but that's far from easy. This can become very complicated very fast, because you're doing the compiler's job.

That's the reason why I never did it in Rustic; I just avoid the "create a resource in function X and return a pointer to the resource" way of doing things. It works in C, if you are meticulous enough to keep track of those pointers yourself, but in Rust, it breaks the compiler's guarantees and you'll have to provide them yourself. If you're going to do that, it's MUCH easier to just use C.

Rule number one when using Rust: don't try to use the language as if it's C, or you'll fight the compiler every step of the way, and your code will become VERY messy.
Interesting! I know basically nothing about Rust (would'nt even know how a hello world looks). But there is a similar issue in C++: of course you can return a pointer to something from a function, which can open a few cans of worms. First of all, you will have to ensure that the object it's pointing to still exists outside of the function, but that isn't so much of a problem, since typically you would return pointers to stuff that has been allocated on the heap. You can however break the RAII paradigm when doing that, since you delegate the responsibility of cleaning up to the calling code. That's the reason smart pointers were introduced in modern C++: you don't return a pointer, but a class that encapsulates the pointer. For the constructor of that class you will have to provide a lambda function that handles deallocation. That way everything is neat and clean again, you stick with RAII and the performance impact is minimal.
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Simplifying code II

Post by mvanthoor »

R. Tomasi wrote: Sat Sep 25, 2021 11:34 am Interesting! I know basically nothing about Rust (would'nt even know how a hello world looks). But there is a similar issue in C++: of course you can return a pointer to something from a function, which can open a few cans of worms. First of all, you will have to ensure that the object it's pointing to still exists outside of the function, but that isn's so much of a problem, since typically you would return pointers to stuff that has been allocated on the heap. You can however break the RAII paradigm when doing that, since you delegate the responsibility of cleaning up to the calling code. That's the reason smart pointers were introduced in modern C++: you don't return a pointer, but a class that encapsulates the pointer. For the constructor of that class you will have to provide a lambda function that handles deallocation. That way everything is neat and clean again, you stick with RAII and the performance impact is minimal.
Rust has smart pointers since almost the beginning ( https://doc.rust-lang.org/book/ch15-00- ... nters.html )

They work differently than C++ smart pointers as far as I can tell (my C++ knowledge is somewhat dated), but seem to be able to do the same things.
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
Henk
Posts: 7221
Joined: Mon May 27, 2013 10:31 am

Re: Simplifying code II

Post by Henk »

Using internal functions or methods to reduce size of method SearchMoves.
How low can you go.
Henk
Posts: 7221
Joined: Mon May 27, 2013 10:31 am

Re: Simplifying code II

Post by Henk »

O wait. It is called local functions in C#.

https://docs.microsoft.com/en-us/dotnet ... -functions
R. Tomasi
Posts: 307
Joined: Wed Sep 01, 2021 4:08 pm
Location: Germany
Full name: Roland Tomasi

Re: Simplifying code II

Post by R. Tomasi »

Henk wrote: Tue Oct 05, 2021 2:38 pm O wait. It is called local functions in C#.

https://docs.microsoft.com/en-us/dotnet ... -functions
Does that have any performance impact over static methods? Only case where I would see some is when you're replacing delegates with locals. Haven't tried if they are faster or not - just a guess of mine.
Joost Buijs
Posts: 1566
Joined: Thu Jul 16, 2009 10:47 am
Location: Almere, The Netherlands

Re: Simplifying code II

Post by Joost Buijs »

Henk wrote: Tue Oct 05, 2021 2:38 pm O wait. It is called local functions in C#.

https://docs.microsoft.com/en-us/dotnet ... -functions
They look a bit like C++ Lambda functions, can be very handy sometimes. Their speed is the same as normal inline functions.
Henk
Posts: 7221
Joined: Mon May 27, 2013 10:31 am

Re: Simplifying code II

Post by Henk »

If I hate myself I can also create an object SearchMoves, assign properties and other misery.