Recent

Author Topic: Exceptions  (Read 1907 times)

440bx

  • Hero Member
  • *****
  • Posts: 1199
Re: Exceptions
« Reply #15 on: July 16, 2019, 12:45:44 am »
@SymbolicFrank

I use try..except when calling a function/method that throws an exception when something goes wrong. It's one way to flag an error, I guess.
Yes, a very poor way of flagging an error.  One that is unfortunately all too common in OOP.


@Handoko,

I always avoid try..except block, I personally think it is not good to show that 'panic' message to users. In the old DOS days I use IOResult, now I use all the think I can to check if the process may go wrong.
What you're doing is the right way of doing it, ensure the entry conditions are as they should be to guarantee the function/procedure can successfully perform its task (whatever it may be.)

I am okay to use try..finally block to free the object/memory
if the program is structured correctly, you don't need any try/finally blocks either.

Maybe not the best, but that it is what I usually do.
You're on the right track, from what you posted, you're quite close to doing it the best way.


I try to keep error messages out of lower level functions.
That's a good practice.  Prevents having error messages being set all over the program instead of in one centralized location where they can be managed.

exceptions are the right tool to use for a program that deals with resources it doesn't own. When used to "protect" against some mishap when dealing with resources it owns, they simply are an indication of poor programming/design.

« Last Edit: July 16, 2019, 01:34:41 am by 440bx »
using FPC v3.0.4 and Lazarus 1.8.2 on Windows 7 64bit.

VTwin

  • Hero Member
  • *****
  • Posts: 787
  • Former Turbo Pascal 3 user
Re: Exceptions
« Reply #16 on: July 16, 2019, 01:17:08 am »
@engkin

To expand a little, I define:

Code: Pascal  [Select]
  1. const
  2.   NAIOFloat : double  = -1.7e+308;   // explicit missing value, i/o = 'NaN'
  3.   NAFloat   : double  = -1.6e+308;   // missing value, i/o = ''
  4.   MaxFloat  : double  = 1.5e+308;    // use +/- as min-max range
  5.  
  6. { IsNA
  7.   'Is Not Available', returns true if number is not valid. }
  8. function  IsNA(r: double): boolean;
  9. begin
  10.   result := (r = NAFloat) or (r = NAIOFloat);
  11. end;
  12.  
  13. { IsNum
  14.   Uses NAFloat, a very large magnitude negative double whose magnitude
  15.   is bigger than MaxFloat. Use NAFloat to initialize doubles, and -MaxFloat for
  16.   smallest double. }
  17. function IsNum(r: double): boolean;
  18. begin
  19.   result := (r > NAFloat);
  20. end;

So I might return NAFloat for your function to indicate it did not work, and check it with IsNum. I find that NAN is not usable cross-platform. Initialized data that is not input yet is set to NAFloat, data that is explicitly not a number is set to NAIOFloat. A spreadsheet of data values displays '' or 'NaN' for those values.
“Talk is cheap. Show me the code.” -Linus Torvalds

macOS 10.13.6: Lazarus 2.0 (2.0.7) fixes svn 62148 (64 bit Cocoa)
Ubuntu 18.04.3: Lazarus 2.0.6 (64 bit on VBox)
Windows 7 Pro SP1: Lazarus 2.0.6 (64 bit on VBox)
fpc 3.0.4

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #17 on: July 16, 2019, 01:50:39 pm »
@SymbolicFrank

I use try..except when calling a function/method that throws an exception when something goes wrong. It's one way to flag an error, I guess.
Yes, a very poor way of flagging an error.  One that is unfortunately all too common in OOP.
CPU exceptions included?

I am okay to use try..finally block to free the object/memory
if the program is structured correctly, you don't need any try/finally blocks either.
Can you elaborate a bit more here?

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #18 on: July 16, 2019, 01:57:14 pm »
@engkin

To expand a little, I define:

Code: Pascal  [Select]
  1. const
  2.   NAIOFloat : double  = -1.7e+308;   // explicit missing value, i/o = 'NaN'
  3.   NAFloat   : double  = -1.6e+308;   // missing value, i/o = ''
  4.   MaxFloat  : double  = 1.5e+308;    // use +/- as min-max range
  5.  
  6. { IsNA
  7.   'Is Not Available', returns true if number is not valid. }
  8. function  IsNA(r: double): boolean;
  9. begin
  10.   result := (r = NAFloat) or (r = NAIOFloat);
  11. end;
  12.  
  13. { IsNum
  14.   Uses NAFloat, a very large magnitude negative double whose magnitude
  15.   is bigger than MaxFloat. Use NAFloat to initialize doubles, and -MaxFloat for
  16.   smallest double. }
  17. function IsNum(r: double): boolean;
  18. begin
  19.   result := (r > NAFloat);
  20. end;

So I might return NAFloat for your function to indicate it did not work, and check it with IsNum. I find that NAN is not usable cross-platform. Initialized data that is not input yet is set to NAFloat, data that is explicitly not a number is set to NAIOFloat. A spreadsheet of data values displays '' or 'NaN' for those values.
Thank you for this explanation. I see that you reduced the range of the type double using MaxFloat, and used two values outside that range to indicate NAIOFloat and NAFloat. Probably for accuracy reasons?

In the general case this is not possible, or not desirable. For instance, consider integer instead of double. How do you handle errors then?

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #19 on: July 16, 2019, 01:59:29 pm »
I always avoid try..except block, I personally think it is not good to show that 'panic' message to users. In the old DOS days I use IOResult, now I use all the think I can to check if the process may go wrong.

This "panic" message is the result of not using, or not knowing how to use exceptions. It happens when you do not "catch" exceptions. That is why I asked how to find all possible exceptions from a call.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 635
Re: Exceptions
« Reply #20 on: July 16, 2019, 02:11:21 pm »
I like this example:

Code: Pascal  [Select]
  1. program test;
  2.  
  3. var
  4.   s: string;
  5. begin
  6.   s := '12345';
  7.   s := Copy(s, -1, MAXINT);
  8.   WriteLn(s);
  9. end.
  10.  

Of course the index and length of the Copy() are invalid, but you still get a result. While many different languages (and different Pascal implementations) would raise an exception, because the result would be incorrect. While strictly true, I like the graceful result.

That raises another question: does your logic keeps working with partial results? That's why you check your inputs.

About returning errors: In C, atoi("bla!") returns 0 (zero). That is bad, because you don't know if an error occurred. You could do: 'BOOL atoi("bla!", ReturnVar)', but that makes for messy code. 'int atoi("bla!", DefaultValue)' is better. And otherwise do raise that exception, and catch it.

440bx

  • Hero Member
  • *****
  • Posts: 1199
Re: Exceptions
« Reply #21 on: July 16, 2019, 02:29:21 pm »
CPU exceptions included?
In user mode - ring 3 - most CPU exceptions are caused by bugs in the program.  An exception (pun intended ;)) are the debug exceptions and those are handled by the OS and routed to the debugger as events.  In a bug-free program that only concerns itself with resources it owns, there should not be any exceptions.

Can you elaborate a bit more here?
Sure.  The most common use of try/finally is to ensure that resources that have been allocated are de-allocated when no longer necessary.

If the variables that hold the handles and/or pointers to those resources are initialized before they are used then a try/finally is unnecessary.  For instance
Code: Pascal  [Select]
  1. function FileMap (Filename : pchar) : pbyte;
  2.   // maps into memory the file to dump
  3. var
  4.   FileHandle           : THANDLE = 0;
  5.   FileMapping          : THANDLE = 0;
  6.  
  7.   p                    : pbyte   = nil;
  8.  
  9.   SCOPE                : integer;
  10.  
  11. const
  12.   FUNCTION_ID          = 'B10: ';
  13.  
  14.   // constants used by CreateFile
  15.  
  16.   NO_TEMPLATE_FILE     =       0;
  17.  
  18.   // constants used by CreateFileMapping
  19.  
  20.   NO_MAXIMUM_SIZE_HIGH =       0;      // 0 indicates to use the size of the
  21.   NO_MAXIMUM_SIZE_LOW  =       0;      //   file
  22.  
  23.   // constants used by MapViewOfFileEx
  24.  
  25.   FILE_OFFSET_HIGH     =       0;      //   file offset to map from
  26.   FILE_OFFSET_LOW      =       0;
  27.  
  28.   BEGINNING_TO_END     =       0;
  29.  
  30. begin
  31.   result := nil;              // default return value
  32.  
  33.   if IsBadStringPtrA(Filename, STRING_MAX_LENGTH)                   then exit;
  34.  
  35.   for SCOPE := 1 to 1 do      // trick to create a scope one can break out of
  36.   begin
  37.     FileHandle := CreateFileA(Filename,
  38.                               GENERIC_READ,
  39.                               FILE_SHARE_READ,
  40.                               nil,
  41.                               OPEN_EXISTING,
  42.                               FILE_ATTRIBUTE_NORMAL,
  43.                               NO_TEMPLATE_FILE);
  44.  
  45.     if FileHandle = INVALID_HANDLE_VALUE then
  46.     begin
  47.       OutputToDeviceA(FUNCTION_ID,
  48.                       'unable to open ', Filename);
  49.       Newline();
  50.       break;
  51.     end;
  52.  
  53.     // with the file handle, create a mapping for it
  54.  
  55.     FileMapping := CreateFileMappingA(FileHandle,
  56.                                       nil,
  57.                                       PAGE_READONLY,
  58.                                       NO_MAXIMUM_SIZE_HIGH,  // use file size
  59.                                       NO_MAXIMUM_SIZE_LOW,
  60.                                       nil);
  61.     if (FileMapping = 0) then
  62.     begin
  63.       OutputToDeviceA(FUNCTION_ID, 'unable to map ', Filename);
  64.       Newline();
  65.       break;
  66.     end;
  67.  
  68.     p := MapViewOfFileEx(FileMapping,
  69.                          FILE_MAP_READ,
  70.                          FILE_OFFSET_HIGH,                   // from beginning
  71.                          FILE_OFFSET_LOW,
  72.                          BEGINNING_TO_END,                   // to end
  73.                          nil);                               // map anywhere
  74.  
  75.     if p = nil then
  76.     begin
  77.       OutputToDeviceA(FUNCTION_ID,
  78.                       'unable to map view of file', Filename);
  79.       Newline();
  80.       break;
  81.  
  82.       // the note below applies only when a non nil address is specified to
  83.       // map at a specific address
  84.  
  85.       // NOTE: MapViewOfFileEx fails if it cannot map at the specified address.
  86.       //       in other words, it will NOT attempt to map to a different address
  87.       //       to succeed.  Because of this there is no need to check if the
  88.       //       address returned is equal to the address specified.
  89.  
  90.       // The documentation states:
  91.  
  92.       // Reserved and committed pages are released when the view is unmapped and
  93.       // the file mapping object is closed. For details, see the UnmapViewOfFile
  94.       // and CloseHandle functions.
  95.  
  96.       // The above means that the file mapping is still valid even after we
  97.       // close the handles for the file and the file mapping.
  98.     end;
  99.   end;
  100.  
  101.   if (FileHandle  <> INVALID_HANDLE_VALUE) then CloseHandle(FileHandle);
  102.   if (FileMapping <>                    0) then CloseHandle(FileMapping);
  103.  
  104.   result := p;
  105. end;
  106.  
The above function creates a scope and releases whatever resources were obtained once the scope is exited.  As long as the variables that hold resource handles/pointers are initialized, it will work fine.

That sequence of code is quite common and, I've seen it quite a few times (Jeffrey Richter's examples for instance) where, if something goes wrong, there are if statements that release whatever resources have been obtained so far but, that's a horrible way of doing it because as more resources are obtained, the number of resources released in every if statement increases.  That is bug prone, forget to release one and there is a leak.  Also, re-using the code is not straightforward because any needed modification may require all those if statements that release resources to be modified.

Using try/finally is very similar to using if statements.  The try/finally(ies) are nested to account for every resource obtained.

if(s) and try/finally are very poor solutions to releasing resources.  A little bit of structuring (with a scope) makes the function fully linear with a minimum amount of nesting (usually, 1 nesting level : the scope)
« Last Edit: July 16, 2019, 02:33:53 pm by 440bx »
using FPC v3.0.4 and Lazarus 1.8.2 on Windows 7 64bit.

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #22 on: July 16, 2019, 02:38:13 pm »
I like this example:

Code: Pascal  [Select]
  1. program test;
  2.  
  3. var
  4.   s: string;
  5. begin
  6.   s := '12345';
  7.   s := Copy(s, -1, MAXINT);
  8.   WriteLn(s);
  9. end.
  10.  
This is a fantastic example.

Of course the index and length of the Copy() are invalid, but you still get a result. While many different languages (and different Pascal implementations) would raise an exception, because the result would be incorrect. While strictly true, I like the graceful result.
Unless ignored, an exception does not necessarily mean ungraceful result.

That raises another question: does your logic keeps working with partial results? That's why you check your inputs.
I like the fact that this question was "raised". Checking your inputs is natural, with or without exceptions. The two are unrelated. You need to check your inputs no matter what.

About returning errors: In C, atoi("bla!") returns 0 (zero). That is bad, because you don't know if an error occurred. You could do: 'BOOL atoi("bla!", ReturnVar)', but that makes for messy code. 'int atoi("bla!", DefaultValue)' is better.
atoi has implicit DefaultValue of zero.

And otherwise do raise that exception, and catch it.
As you can see, sometimes it is a matter of style, but not always.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 635
Re: Exceptions
« Reply #23 on: July 16, 2019, 02:56:26 pm »
atoi has implicit DefaultValue of zero.

Yes, but that is what prevents you from spotting errors in most cases. MININT would be a much better default. It depends on the strings you try to translate.

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #24 on: July 16, 2019, 04:03:39 pm »
CPU exceptions included?
In user mode - ring 3 - most CPU exceptions are caused by bugs in the program. 
Did you say most?

An exception (pun intended ;)) are the debug exceptions and those are handled by the OS and routed to the debugger as events. 
I see exceptions on top of exceptions.

In a bug-free program that only concerns itself with resources it owns, there should not be any exceptions.
A bug-free program is a program that does nothing, whence it has no bugs. Joking aside, apart from small samples, programs are always dealing with resources they do not own, or depend on libraries that do use exceptions. Exceptions are just a tool. Like any tool, they need to be used right.

Can you elaborate a bit more here?
Sure.  The most common use of try/finally is to ensure that resources that have been allocated are de-allocated when no longer necessary.
While this usage is the common one, it is not the only reason. You can use try/finally to signal the end of a process, or change the status, or anything else that you want to make sure is done at the end of a try finally block.

If the variables that hold the handles and/or pointers to those resources are initialized before they are used then a try/finally is unnecessary.  For instance
Code: Pascal  [Select]
  1. function FileMap (Filename : pchar) : pbyte;
  2.   // maps into memory the file to dump
  3. var
  4.   FileHandle           : THANDLE = 0;
  5.   FileMapping          : THANDLE = 0;
  6.  
  7.   p                    : pbyte   = nil;
  8.  
  9.   SCOPE                : integer;
  10.  
  11. const
  12.   FUNCTION_ID          = 'B10: ';
  13.  
  14.   // constants used by CreateFile
  15.  
  16.   NO_TEMPLATE_FILE     =       0;
  17.  
  18.   // constants used by CreateFileMapping
  19.  
  20.   NO_MAXIMUM_SIZE_HIGH =       0;      // 0 indicates to use the size of the
  21.   NO_MAXIMUM_SIZE_LOW  =       0;      //   file
  22.  
  23.   // constants used by MapViewOfFileEx
  24.  
  25.   FILE_OFFSET_HIGH     =       0;      //   file offset to map from
  26.   FILE_OFFSET_LOW      =       0;
  27.  
  28.   BEGINNING_TO_END     =       0;
  29.  
  30. begin
  31.   result := nil;              // default return value
  32.  
  33.   if IsBadStringPtrA(Filename, STRING_MAX_LENGTH)                   then exit;
  34.  
  35.   for SCOPE := 1 to 1 do      // trick to create a scope one can break out of
  36.   begin
  37.     FileHandle := CreateFileA(Filename,
  38.                               GENERIC_READ,
  39.                               FILE_SHARE_READ,
  40.                               nil,
  41.                               OPEN_EXISTING,
  42.                               FILE_ATTRIBUTE_NORMAL,
  43.                               NO_TEMPLATE_FILE);
  44.  
  45.     if FileHandle = INVALID_HANDLE_VALUE then
  46.     begin
  47.       OutputToDeviceA(FUNCTION_ID,
  48.                       'unable to open ', Filename);
  49.       Newline();
  50.       break;
  51.     end;
  52.  
  53.     // with the file handle, create a mapping for it
  54.  
  55.     FileMapping := CreateFileMappingA(FileHandle,
  56.                                       nil,
  57.                                       PAGE_READONLY,
  58.                                       NO_MAXIMUM_SIZE_HIGH,  // use file size
  59.                                       NO_MAXIMUM_SIZE_LOW,
  60.                                       nil);
  61.     if (FileMapping = 0) then
  62.     begin
  63.       OutputToDeviceA(FUNCTION_ID, 'unable to map ', Filename);
  64.       Newline();
  65.       break;
  66.     end;
  67.  
  68.     p := MapViewOfFileEx(FileMapping,
  69.                          FILE_MAP_READ,
  70.                          FILE_OFFSET_HIGH,                   // from beginning
  71.                          FILE_OFFSET_LOW,
  72.                          BEGINNING_TO_END,                   // to end
  73.                          nil);                               // map anywhere
  74.  
  75.     if p = nil then
  76.     begin
  77.       OutputToDeviceA(FUNCTION_ID,
  78.                       'unable to map view of file', Filename);
  79.       Newline();
  80.       break;
  81.  
  82.       // the note below applies only when a non nil address is specified to
  83.       // map at a specific address
  84.  
  85.       // NOTE: MapViewOfFileEx fails if it cannot map at the specified address.
  86.       //       in other words, it will NOT attempt to map to a different address
  87.       //       to succeed.  Because of this there is no need to check if the
  88.       //       address returned is equal to the address specified.
  89.  
  90.       // The documentation states:
  91.  
  92.       // Reserved and committed pages are released when the view is unmapped and
  93.       // the file mapping object is closed. For details, see the UnmapViewOfFile
  94.       // and CloseHandle functions.
  95.  
  96.       // The above means that the file mapping is still valid even after we
  97.       // close the handles for the file and the file mapping.
  98.     end;
  99.   end;
  100.  
  101.   if (FileHandle  <> INVALID_HANDLE_VALUE) then CloseHandle(FileHandle);
  102.   if (FileMapping <>                    0) then CloseHandle(FileMapping);
  103.  
  104.   result := p;
  105. end;
  106.  
The above function creates a scope and releases whatever resources were obtained once the scope is exited.  As long as the variables that hold resource handles/pointers are initialized, it will work fine.

That sequence of code is quite common and, I've seen it quite a few times (Jeffrey Richter's examples for instance) where, if something goes wrong, there are if statements that release whatever resources have been obtained so far but, that's a horrible way of doing it because as more resources are obtained, the number of resources released in every if statement increases.  That is bug prone, forget to release one and there is a leak.  Also, re-using the code is not straightforward because any needed modification may require all those if statements that release resources to be modified.
Wait, this example is supposed to show how you do not need try/finally?
You used a for loop to create a scope so you can use break?
You want to use break but avoid try/finally?
You avoided using objects in your function. You do realize that you can achieve the same goal using objects, specially when this function get complicated, right?

Using try/finally is very similar to using if statements.  The try/finally(ies) are nested to account for every resource obtained.
You can replace one with the other, but they are not the same.

The try/finally(ies) are nested to account for every resource obtained.
Not necessarily.

if(s) and try/finally are very poor solutions to releasing resources.  A little bit of structuring (with a scope) makes the function fully linear with a minimum amount of nesting (usually, 1 nesting level : the scope)
Can you show how you do that with the function you showed above?
Why do you want to keep nesting at minimum?

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #25 on: July 16, 2019, 04:20:34 pm »
atoi has implicit DefaultValue of zero.

Yes, but that is what prevents you from spotting errors in most cases. MININT would be a much better default. It depends on the strings you try to translate.
It is not there to help you spot errors, nor does is prevent you from spotting errors. It does what it is supposed to do: translate any string to number. Zero is the default. If you want to "spot" errors, use a different function.

Using the wrong tool and complaining it is not doing what you want is not right.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 635
Re: Exceptions
« Reply #26 on: July 16, 2019, 04:42:51 pm »
atoi has implicit DefaultValue of zero.

Yes, but that is what prevents you from spotting errors in most cases. MININT would be a much better default. It depends on the strings you try to translate.
It is not there to help you spot errors, nor does is prevent you from spotting errors. It does what it is supposed to do: translate any string to number. Zero is the default. If you want to "spot" errors, use a different function.

Using the wrong tool and complaining it is not doing what you want is not right.

It was the example I used to explain the issue, and I did offer solutions. And yes, I did write my own function, because there were no functions that did what I wanted. Even more so: I have a hard time coming up with a use case where the default atoi function would make sense.

440bx

  • Hero Member
  • *****
  • Posts: 1199
Re: Exceptions
« Reply #27 on: July 16, 2019, 05:07:42 pm »
Did you say most?
yes, I did.  Among the CPU exceptions are "invalid opcode" exceptions which can happen if there is a bug in the compiler one is using, not in the program.  Also, these days, programmers are raising exceptions because they buried themselves in a pile of function/procedure calls and the only way they have left to inform the original caller something went wrong is by raising an exception (extremely poor programming!.)

I see exceptions on top of exceptions.
I see lots of those in OOP programs.

apart from small samples, programs are always dealing with resources they do not own,
By resources they don't own, I mean resources whose existence they cannot predict.  For instance, if a program is attempting to read memory that is in another process (or the same process but, is not managed by the program), the program cannot make any assumptions as to the presence of memory at a particular address.  That said, that example with memory is not very good since using Read/WriteProcessMemory eliminates the need for an exception handler.  The basic rule is, when the proper operation of a set of instructions cannot be predicted then, exceptions are warranted, otherwise the programmer should write clean code, which means do all the necessary checking before carrying out the function.


or depend on libraries that do use exceptions.
That's defines the typical case: a poorly written library spreads its poor design to the program that wants to use it.  Apparently, it's too much work these days to return an indication of failure or success as a function result.  Using exceptions makes a programmer look "kewl" and knowledgeable.  Function results ?... way too prosaic these days.

Exceptions are just a tool. Like any tool, they need to be used right.
I agree with that.  These days they are grossly abused and misused.


While this usage is the common one, it is not the only reason. You can use try/finally to signal the end of a process, or change the status, or anything else that you want to make sure is done at the end of a try finally block.
You're right, it's not the only reason.  That was just an example and I picked something quite common for it.   I can write much simpler, easier to understand and follow code without using try/finally.

Wait, this example is supposed to show how you do not need try/finally?
That's one example. I'd much rather code it like that than use a try/finally construct.  It is a lot cleaner.  No nested try/finally and when debugging, the execution doesn't jump around into some finally(ies) if something didn't go as expected.

You used a for loop to create a scope so you can use break?
Yes, unfortunately.  There are no scopes in Pascal.   That is not as clean as I'd like it to be but, it's still way better than try/finally(ies.)

You want to use break but avoid try/finally?
That's only one of the reasons.  Another reason is that, that way, all the resource deallocation is in _one_ place, not scattered all over.   In more complex functions, that makes a very noticeable difference, particularly when debugging (the debugger isn't jumping around from one finally to another.)

You avoided using objects in your function. You do realize that you can achieve the same goal using objects, specially when this function get complicated, right?
I strongly believe that ANYTHING that can be written using objects can be written in a clearer, simpler, easier to maintain and more efficient way without objects.  If there is an exception to that, I am yet to see it.

That said, if you'd like to rewrite that function using objects and point out the reasons why yours is better than the one I posted, I would be very pleased to see it.

Using try/finally is very similar to using if statements.  The try/finally(ies) are nested to account for every resource obtained.
You can replace one with the other, but they are not the same.
That's true but, as a programming method, they have some flaws in common.

The try/finally(ies) are nested to account for every resource obtained.
Not necessarily.
True but, quite often, they are.

if(s) and try/finally are very poor solutions to releasing resources.  A little bit of structuring (with a scope) makes the function fully linear with a minimum amount of nesting (usually, 1 nesting level : the scope)
Can you show how you do that with the function you showed above?
Why do you want to keep nesting at minimum?
The function already uses a scope (unfortunately, with trickery since there is no support for it in the language.)  The reason I keep nesting at a minimum is because the lower the level of nesting the easier a construction is to understand.
using FPC v3.0.4 and Lazarus 1.8.2 on Windows 7 64bit.

engkin

  • Hero Member
  • *****
  • Posts: 2513
Re: Exceptions
« Reply #28 on: July 16, 2019, 07:05:39 pm »
Among the CPU exceptions are "invalid opcode" exceptions which can happen if there is a bug in the compiler one is using, not in the program.
Or using the program on a different CPU that lacks the "invalid opcode"?

  Also, these days, programmers are raising exceptions because they buried themselves in a pile of function/procedure calls and the only way they have left to inform the original caller something went wrong is by raising an exception (extremely poor programming!.)
Keeping all the code in one function and returning false or any value to indicate the exact error is not much different. Taking either extreme is simply poor programming practice, and does not help readability nor prevents bugs.

Using enough functions that return error codes, or objects that raise exceptions carrying error codes and messages, both are simply two styles of programming. You can abuse either one.

I see exceptions on top of exceptions.
I see lots of those in OOP programs.
You should, it is just a style of programming.

apart from small samples, programs are always dealing with resources they do not own,
By resources they don't own, I mean resources whose existence they cannot predict.  For instance, if a program is attempting to read memory that is in another process (or the same process but, is not managed by the program), the program cannot make any assumptions as to the presence of memory at a particular address.  That said, that example with memory is not very good since using Read/WriteProcessMemory eliminates the need for an exception handler.  The basic rule is, when the proper operation of a set of instructions cannot be predicted then, exceptions are warranted,
It is hard to imagine an applications that does not deal with the internet, json, networks, databases, acquired data from wired/wireless sources like images or ... etc.

otherwise the programmer should write clean code, which means do all the necessary checking before carrying out the function.
This applies when using OOP or not.

or depend on libraries that do use exceptions.
That's defines the typical case: a poorly written library spreads its poor design to the program that wants to use it.  Apparently, it's too much work these days to return an indication of failure or success as a function result.  Using exceptions makes a programmer look "kewl" and knowledgeable.  Function results ?... way too prosaic these days.
I hear you, you prefer to use a library and with each call to check the result: Initialize the library, check. A call, a check. Another call, another check, repeat... , finalize the library, then check.

You don't like to include all of that in one block and leave the checking to the library itself? It would keep your code focused on its own part. But again, this sounds like your own style of programming, I have nothing against it. I can imagine how if thisVar=someErrorValue then grows in your code.

I want to let you know that JAVA does have exceptions and you have to catch them, and, unfortunately, FPC can produce JAVA classes, run on JVMs, and interact with other JAVA classes.

Exceptions are just a tool. Like any tool, they need to be used right.
I agree with that.  These days they are grossly abused and misused.
Anything could be abused of misused, does not mean we need to avoid it, or consider it poor practice.

While this usage is the common one, it is not the only reason. You can use try/finally to signal the end of a process, or change the status, or anything else that you want to make sure is done at the end of a try finally block.
You're right, it's not the only reason.  That was just an example and I picked something quite common for it.   I can write much simpler, easier to understand and follow code without using try/finally.
Yes, until you are forced.

Wait, this example is supposed to show how you do not need try/finally?
That's one example. I'd much rather code it like that than use a try/finally construct.  It is a lot cleaner.  No nested try/finally and when debugging, the execution doesn't jump around into some finally(ies) if something didn't go as expected.
The debugger, when enough debugging data is available, is supposed to show you exactly where the exception had happened before it takes you anywhere else. But if you don't like try/finally for that reason, then you do not like else either because it jump somewhere far from the line the debugger was on.

You used a for loop to create a scope so you can use break?
Yes, unfortunately.  There are no scopes in Pascal.   That is not as clean as I'd like it to be but, it's still way better than try/finally(ies.)
It is not clean, and not supported by Pascal, but you used it. While Try/Finally is part of the language, the object oriented dialect, and you refuse to use it?

You want to use break but avoid try/finally?
That's only one of the reasons.  Another reason is that, that way, all the resource deallocation is in _one_ place, not scattered all over.   In more complex functions, that makes a very noticeable difference, particularly when debugging (the debugger isn't jumping around from one finally to another.)
"Scattered" or not, if deallocation happens in a logical and correct way, and always guaranteed there is no problem. The desire to have all the deallocations in one place came with extra price, at least, the chain of IF THEN. Again, a style of programming.

You avoided using objects in your function. You do realize that you can achieve the same goal using objects, specially when this function get complicated, right?
I strongly believe that ANYTHING that can be written using objects can be written in a clearer, simpler, easier to maintain and more efficient way without objects.  If there is an exception to that, I am yet to see it.
Yes, a call to member function of an object like AStringList.Add is actually a call to a function with the first parameter is the object itself: Add(AStringList,... see?
It is like adjectives are before/after the nouns in different languages:
English (Brown Horse)
Spanish (Horse Brown)
Which one do you prefer?

That said, if you'd like to rewrite that function using objects and point out the reasons why yours is better than the one I posted, I would be very pleased to see it.
I might give it a shot, not sure.

if(s) and try/finally are very poor solutions to releasing resources.  A little bit of structuring (with a scope) makes the function fully linear with a minimum amount of nesting (usually, 1 nesting level : the scope)
Can you show how you do that with the function you showed above?
Why do you want to keep nesting at minimum?
The function already uses a scope (unfortunately, with trickery since there is no support for it in the language.)  The reason I keep nesting at a minimum is because the lower the level of nesting the easier a construction is to understand.
When the level of nesting grows, it is time to consider another function/member function.

Now I believe that you have your own style of programming and refuse to change it, not because it protects you from bugs, but simply because you do not want to try something new. Nothing unusual here, but considering other ways as poor or more prone to bugs is not accurate.

Handoko

  • Hero Member
  • *****
  • Posts: 3188
  • My goal: build my own game engine using Lazarus
Re: Exceptions
« Reply #29 on: July 16, 2019, 08:07:48 pm »
For instance
Code: Pascal  [Select]
  1. function FileMap (Filename : pchar) : pbyte;
  2.   // maps into memory the file to dump
  3. var
  4.   FileHandle           : THANDLE = 0;
  5.   FileMapping          : THANDLE = 0;
  6.  
  7.   p                    : pbyte   = nil;
  8.  
  9.   SCOPE                : integer;
  10.  
  11. const
  12.   FUNCTION_ID          = 'B10: ';
  13.  
  14.   // constants used by CreateFile
  15.  
  16.   NO_TEMPLATE_FILE     =       0;
  17.  
  18.   // constants used by CreateFileMapping
  19.  
  20.   NO_MAXIMUM_SIZE_HIGH =       0;      // 0 indicates to use the size of the
  21.   NO_MAXIMUM_SIZE_LOW  =       0;      //   file
  22.  
  23.   // constants used by MapViewOfFileEx
  24.  
  25.   FILE_OFFSET_HIGH     =       0;      //   file offset to map from
  26.   FILE_OFFSET_LOW      =       0;
  27.  
  28.   BEGINNING_TO_END     =       0;
  29.  
  30. begin
  31.   result := nil;              // default return value
  32.  
  33.   if IsBadStringPtrA(Filename, STRING_MAX_LENGTH)                   then exit;
  34.  
  35.   for SCOPE := 1 to 1 do      // trick to create a scope one can break out of
  36.   begin
  37.     FileHandle := CreateFileA(Filename,
  38.                               GENERIC_READ,
  39.                               FILE_SHARE_READ,
  40.                               nil,
  41.                               OPEN_EXISTING,
  42.                               FILE_ATTRIBUTE_NORMAL,
  43.                               NO_TEMPLATE_FILE);
  44.  
  45.     if FileHandle = INVALID_HANDLE_VALUE then
  46.     begin
  47.       OutputToDeviceA(FUNCTION_ID,
  48.                       'unable to open ', Filename);
  49.       Newline();
  50.       break;
  51.     end;
  52.  
  53.     // with the file handle, create a mapping for it
  54.  
  55.     FileMapping := CreateFileMappingA(FileHandle,
  56.                                       nil,
  57.                                       PAGE_READONLY,
  58.                                       NO_MAXIMUM_SIZE_HIGH,  // use file size
  59.                                       NO_MAXIMUM_SIZE_LOW,
  60.                                       nil);
  61.     if (FileMapping = 0) then
  62.     begin
  63.       OutputToDeviceA(FUNCTION_ID, 'unable to map ', Filename);
  64.       Newline();
  65.       break;
  66.     end;
  67.  
  68.     p := MapViewOfFileEx(FileMapping,
  69.                          FILE_MAP_READ,
  70.                          FILE_OFFSET_HIGH,                   // from beginning
  71.                          FILE_OFFSET_LOW,
  72.                          BEGINNING_TO_END,                   // to end
  73.                          nil);                               // map anywhere
  74.  
  75.     if p = nil then
  76.     begin
  77.       OutputToDeviceA(FUNCTION_ID,
  78.                       'unable to map view of file', Filename);
  79.       Newline();
  80.       break;
  81.  
  82.       // the note below applies only when a non nil address is specified to
  83.       // map at a specific address
  84.  
  85.       // NOTE: MapViewOfFileEx fails if it cannot map at the specified address.
  86.       //       in other words, it will NOT attempt to map to a different address
  87.       //       to succeed.  Because of this there is no need to check if the
  88.       //       address returned is equal to the address specified.
  89.  
  90.       // The documentation states:
  91.  
  92.       // Reserved and committed pages are released when the view is unmapped and
  93.       // the file mapping object is closed. For details, see the UnmapViewOfFile
  94.       // and CloseHandle functions.
  95.  
  96.       // The above means that the file mapping is still valid even after we
  97.       // close the handles for the file and the file mapping.
  98.     end;
  99.   end;
  100.  
  101.   if (FileHandle  <> INVALID_HANDLE_VALUE) then CloseHandle(FileHandle);
  102.   if (FileMapping <>                    0) then CloseHandle(FileMapping);
  103.  
  104.   result := p;
  105. end;
  106.  

You used a for loop to create a scope so you can use break?
Yes, unfortunately.  There are no scopes in Pascal.   That is not as clean as I'd like it to be but, it's still way better than try/finally(ies.)

I never thought to use a 'fake' loop to create a scope. Interesting trick, I learn a new trick today. Thank you.