Saving and Stack Overflows - delphi

I'm having a problem saving a vary large database type in Delphi. It contains an array[1..3500] of TItem, which in turn has two arrays[1..50] and [1..20]. I get a stack overflow unless I set the variable as a Pointer and use the GetMem, FreeMem commands below, but then I can't save it. Code is below.
procedure TDatabase.SaveDB;
var
TempDB: ^TSaveDB;
K, X: integer;
sComment, sTitle, sComposer, sISDN, sCategory: string;
begin
GetMem(TempDB, SizeOf(TSaveDB));
TempDB.CatCount := fCategoryCount;
TempDB.ItemCount := fItemCount;
for K := 1 to fCategoryCount do
TempDB.Categories[K] := fCategories[K];
for K := 1 to fItemCount do
begin
fItems[K].ReturnSet(sTitle, sComposer, sCategory, sISDN, sComment);
with TempDB.Items[K] do
begin
Title := sTitle;
Composer := sComposer;
Category := sCategory;
ISDN := sISDN;
end;
TempDB.Items[K].Comments[1] := Copy(sComment, 1, 255);
Delete(sComment, 1, 255);
TempDB.Items[K].Comments[2] := Copy(sComment, 1, 255);
Delete(sComment, 1, 255);
TempDB.Items[K].Comments[3] := Copy(sComment, 1, 255);
Delete(sComment, 1, 255);
TempDB.Items[K].Comments[4] := Copy(sComment, 1, 255);
Delete(sComment, 1, 255);
TempDB.Items[K].KeyWCount := fItems[K].GetKeyCount;
for X := 1 to fItems[K].GetKeyCount do
TempDB.Items[K].Keywords[X] := fItems[K].GetKeywords(X);
end;
AssignFile(DBSave, fSaveName);
Rewrite(DBSave);
Write(DBSave, TempDB);
Closefile(dBSave);
FreeMem(TempDB, sizeof(TSaveDB));
end;

Use GetMem or SetLength or TList/TObjectList and write to the file one TSaveDB at a time. Or
change the file type and use BlockWrite to write it all at once. Or even better: use TFileStream.

Your problem is in the "write" statement. Doing things with arbitrary pointers leads to all sorts of strange behavior. You'd have it a lot easier if you rewrote this using a TFileStream instead of the current approach.

To expand on Mason's answer:
NEVER read or write a pointer, period. It would take a major stroke of luck to get anything reasonable out of doing it and in the real world when you're not just running your program again the odds of success go from infinitesimal to zero.
Rather, you need to read and write what the pointer points to.
Note, also, that any string whose length isn't named in the declaration is a pointer unless you're running in the compatibility mode that makes "string" into "string[255]"--this mode exists only for compatibility with very old code that was written when this was the only strings we had.
Since you appear to be simply writing the whole thing out there's no reason to be playing games with fixed size records. Simply write each field to a stream, write the length of a string before writing the string itself so you can load it back in properly. The file will be smaller and nothing gets truncated.
Also, as he says, use tFileStream. The old format has it's uses for a file of records that is left on disk, there's no reason to use it in a case like this.

Related

Infinite Loop in Delphi Procedure

I'm having a weird issue on using Delphi's TMemoryStream (or TFileStream for that matter). While reading a part of the stream into a byte array. Here's some code as an example.
procedure readfromstream();
var
ms : TMemoryStream;
buffer : array of byte;
recordSize : Integer;
begin
try
begin
ms := TMemeoryStream.Create();
ms.LoadFromFile(<some_path_to_a_binary_file>);
while ms.Position < ms.Size do
begin
buffer := nil;
SetLength(buffer, 4);
ms.ReadBuffer(buffer, 4);
move(buffer[0], recordSize, 4);
SetLength(buffer, recordSize);
ms.Position := ms.Position - 4; // Because I was having issues trying to read the rest of the record into a specific point in the buffer
FillChar(buffer, recordSize, ' ');
ms.ReadBuffer(buffer, recordSize); // Issue line ???
// Create the record from the buffer
end;
finally
begin
ms.Free();
end;
end;
procedure is called as,
// Some stuff happens before it
readfromstream();
// Some stuff happens after it
on debugging, I can see that it reads the stream into the buffer and the record is stored in memory appropriately. The procedure then exits normally and the debugger steps out of the procedure, but I end up straight back into the procedure and it repeats.
By forcing the procedure to exit prematurely I believe the issue involves the ms.ReadBuffer(buffer, recordSize); but I don't see why it would cause the issue.
This procedure is called only once. My test data has only one entry/data.
Any help would be greatly appreciated.
FillChar(buffer, recordSize, ' ');
Here you are overwriting the dynamic array variable, a pointer, rather than writing to the content of the array. That causes a memory corruption. Pretty much anything goes at that point.
The call to FillChar is needless anyway. You are going to read into the entire array anyway. Remove the call to FillChar.
For future reference, to do that call correctly, you write it like this:
FillChar(Pointer(buffer)^, ...);
or
FillChar(buffer[0], ...);
I prefer the former since the latter is subject to range errors when the array length is zero.
And then
ms.ReadBuffer(buffer, recordSize);
makes the exact same mistake, writing to the array variable rather than the array, and thus corrupting memory.
That should be
ms.ReadBuffer(Pointer(buffer)^, recordSize);
or
ms.ReadBuffer(buffer[0], recordSize);
The first 4 lines inside the loop are clumsy. Read directly into the variable:
ms.ReadBuffer(recordSize, SizeOf(recordSize));
I recommend that you perform some sanity checks on the value of recordSize that you read. For instance, any value less than 4 is clearly an error.
There's not a lot of point in moving the stream pointer back and reading again. You can copy recordSize into the first 4 bytes and the array and then read the rest.
Move(recordSize, buffer[0], SizeOf(recordSize));
ms.ReadBuffer(buffer[SizeOf(recordSize)], recordSize - SizeOf(recordSize));
A memory stream also seems wasteful. Why read the entire file into memory? That's going to place stress on your address space for large files. Use a buffered file stream.
Letting the caller allocate the stream would give more flexibility to the caller. They could then read from any type of stream and not be constrained to use a disk file.
Your try/finally block is wrong. You must acquire the resource immediately before the try. As you have it, an exception in the constructor leads to you calling Free on an uninitialized variable.
A better version might be:
procedure ReadFromStream(Stream: TStream);
var
buffer: TArray<byte>;
recordSize: Integer;
begin
while Stream.Position < Stream.Size do
begin
Stream.ReadBuffer(recordSize, SizeOf(recordSize));
if recordSize < SizeOf(recordSize) then
raise ...;
SetLength(buffer, recordSize);
Move(recordSize, buffer[0], SizeOf(recordSize));
if recordSize > SizeOf(recordSize) then
Stream.ReadBuffer(buffer[SizeOf(recordSize)],
recordSize - SizeOf(recordSize));
// process record
end;
end;
Sorry I can't add a comment, being a newb and all :) This reply is based on my understanding of Clayton's code in light of his comment with the recordSize values.
The reason David's code is looping is probably that you are interpreting each four byte "block" is a number. I'll assume your first Stream.Readbuffer is correct and that the first four bytes in the file is a length.
Now, unless I'm mistaken, I expect the recordSize will usually be greater than SizeOf(recordSize), which I think should be 4 (the size of an int). Nevertheless, this line is meaningless here.
The SetLength is correct, given my previous assumption.
Now your Move is where the story hits a snag: you haven't read anything since you read the length! So before the move, you should have:
bytesRead := Stream.Readbuffer(Pointer(buffer)^, recordSize);
Now you can check for EOF:
if bytesRead <> recordSize then
raise...;
...and move the buffer somewhere (if you wish):
Move(buffer[0], dest[0], recordSize);
And you are positioned to read the next recordSize value.
Repeat until EOF.

Invalid Pointer Operation on dynamic array free

I'm pretty newbie to Delphi, so be kind please.
I'm working on a software that allows users to customize its interface ( button's location, appearance, wtv) and some other stuff.
The problem is, i have a grid and i store a representation of its cells on a dynamic array of Boolean which represents which cells are occupied. But when i try to free that matrix Sometimes i get an invalid pointer operation. Sometimes there isnt any error, but other times i get that invalid pointer stuff.
Definition:
type
TMatrix = array of array of Boolean;
var
Matrix: TMatrix;
Initialization:
SetLength(Matrix, MyGrid.ColumnCollection.Count, MyGrid.RowCollection.Count);
Usage:
Mostly, these kind of operations are the only ones that i use with the matrix, and i'm sure that those variables col,row,colspan,rowspan never have values greater than the array boundary
//Checks if a set of cells are occupied
for i := column to (column + columnspan)-1 do
begin
for j := row to (row + rowspan)-1 do
begin
if Matrix[i, j] then
begin
valido := False;
end;
end;
end;
// if there isnt any cell occupied i can move a component to that location and i set the cells to true ( occupied)
if (valido) then
begin
for i := column to (column + colspan)-1 do
begin
for j := row to (row + rowspan)-1 do
begin
Matrix[i,j]:= True;
end;
end;
end
Free:
try
begin
SetLength(Matrix,0,0);
end;
except
on E : Exception do
begin
//todo
ShowMessage('Free: ' + E.Message);
end;
end;
I'm using FASTMM4 and i do get the memory leak warning, but i cant understand its contents..
What can possibly be causing this error?
Im using Delphi Xe6 Firemonkey
Any help will be appreciated.
Thanks
The most likely explanation for this is that you are writing outside the bounds of the array. There's not enough code for us to be sure that is the case, but the symptoms you report are invariably caused by out-of-bounds access.
Your next step is to get the compiler to write code that checks for our-of-bounds access. In your project options, in the compiler section, find the range checking option. Enable this option. Now the compiler will emit code that checks that your array indices are valid whenever you access the array. If your code fails the test, at runtime, an exception will be raised. This will make it blatantly obvious which part of your code is defective.

Why are strings truncated when using direct printing?

I'm trying to print directly to a printer using esc/p commands (EPSON TM-T70) without using printer driver. Code found here.
However, if I try to print any strings, they are truncated. For example:
MyPrinter := TRawPrint.Create(nil);
try
MyPrinter.DeviceName := 'EPSON TM-T70 Receipt';
MyPrinter.JobName := 'MyJob';
if MyPrinter.OpenDevice then
begin
MyPrinter.WriteString('This is page 1');
MyPrinter.NewPage;
MyPrinter.WriteString('This is page 2');
MyPrinter.CloseDevice;
end;
finally
MyPrinter.Free;
end;
Would print only "This isThis is"! I wouldn't ordinarily use MyPrinter.NewPage to send a line break command, but regardless, why does it truncates the string?
Also notice in RawPrint unit WriteString function:
Result := False;
if IsOpenDevice then begin
Result := True;
if not WritePrinter(hPrinter, PChar(Text), Length(Text), WrittenChars) then begin
RaiseError(GetLastErrMsg);
Result := False;
end;
end;
If I put a breakpoint there and step through the code, then WrittenChars is set to 14, which is correct. Why does it act like that?
You are using a unicode-enabled version of Delphi. Chars are 2 bytes long. When you call your function with Length(s) you're sending the number of chars, but the function probably expects the size of the buffer. Replace it with SizeOf(s) Length(s)*SizeOf(Char).
Since the size of one unicode char is exactly 2 bytes, when you're sending Length when buffer size is required, you're essentially telling the API to only use half the buffer. Hence all strings are aproximately split in half.
Maybe you can use the ByteLength function which gives the length of a string in bytes.

Delphi Pascal - Using SetFilePointerEx and GetFileSizeEx, Getting Physical Media exact size when reading as a file

I do not know how to use any API that is not in the RTL. I have been using SetFilePointer and GetFileSize to read a Physical Disk into a buffer and dump it to a file, something like this in a loop does the job for flash memory cards under 2GB:
SetFilePointer(PD,0,nil,FILE_BEGIN);
SetLength(Buffer,512);
ReadFile(PD,Buffer[0],512,BytesReturned,nil);
However GetFileSize has a limit at 2GB and so does SetFilePointer. I have absolutley no idea how to delcare an external API, I have looked at the RTL and googled for many examples and have found no correct answer.
I tried this
function GetFileSizeEx(hFile: THandle; lpFileSizeHigh: Pointer): DWORD;
external 'kernel32';
and as suggested this
function GetFileSizeEx(hFile: THandle; var FileSize: Int64): DWORD;
stdcall; external 'kernel32';
But the function returns a 0 even though I am using a valid disk handle which I have confirmed and dumped data from using the older API's.
I am using SetFilePointer to jump every 512 bytes and ReadFile to write into a buffer, in reverse I can use it to set when I am using WriteFile to write Initial Program Loader Code or something else to the disk. I need to be able to set the file pointer beyond 2gb well beyond.
Can someone help me make the external declarations and a call to both GetFileSizeEx and SetFilePointerEx that work so I can modify my older code to work with say 4 to 32gb flash cards.
I suggest that you take a look at this Primoz Gabrijelcic blog article and his GpHugeFile unit which should give you enough pointers to get the file size.
Edit 1 This looks rather a daft answer now in light of the edit to the question.
Edit 2 Now that this answer has been accepted, following a long threads of comments to jachguate's answer, I feel it incumbent to summarise what has been learnt.
GetFileSize and
SetFilePointer have no 2GB
limitation, they can be used on files
of essentially arbitrary size.
GetFileSizeEx and
SetFilePointerEx are much
easier to use because they work
directly with 64 bit quantities and
have far simpler error condition
signals.
The OP did not in fact need to
calculate the size of his disk. Since
the OP was reading the entire
contents of the disk the size was not
needed. All that was required was to
read the contents sequentially until
there was nothing left.
In fact
GetFileSize/GetFileSizeEx
do not support handles to devices
(e.g. a physical disk or volume) as
was requested by the OP. What's more,
SetFilePointer/SetFilePointerEx
cannot seek to the end of such device
handles.
In order to obtain the size of a
disk, volume or partition, one should
pass the the
IOCTL_DISK_GET_LENGTH_INFO
control code to
DeviceIoControl.
Finally, should you need to use GetFileSizeEx and SetFilePointerEx then they can be declared as follows:
function GetFileSizeEx(hFile: THandle; var lpFileSize: Int64): BOOL;
stdcall; external 'kernel32.dll';
function SetFilePointerEx(hFile: THandle; liDistanceToMove: Int64;
lpNewFilePointer: PInt64; dwMoveMethod: DWORD): BOOL;
stdcall; external 'kernel32.dll';
One easy way to obtain these API imports them is through the excellent JEDI API Library.
The GetFileSizeEx routine expects a pointer to a LARGE_INTEGER data type, and documentation says:
If your compiler has built-in support for 64-bit integers, use the QuadPart member to store the 64-bit integer
Lucky you, Delphi has built-in support for 64 bit integers, so use it:
var
DriveSize: LongWord;
begin
GetFilePointerSizeEx(PD, #DriveSize);
end;
SetFilePointerEx, on the other hand, expects parameters for liDistanceToMove, lpNewFilePointer, both 64 bit integers. My understanding is it wants signed integers, but you have the UInt64 data type for Unsingned 64 bit integers if I'm missunderstanding the documentation.
Alternative coding
Suicide, first of all your approach is wrong, and because of your wrong approach you ran into some hairy problems with the way Windows handles Disk drives opened as files. In pseudo code your approach seems to be:
Size = GetFileSize;
for i=0 to (Size / 512) do
begin
Seek(i * 512);
ReadBlock;
WriteBlockToFile;
end;
That's functionally correct, but there's a simpler way to do the same without actually getting the SizeOfDisk and without seeking. When reading something from a file (or a stream), the "pointer" is automatically moved with the ammount of data you just read, so you can skip the "seek". All the functions used to read data from a file return the amount of data that was actually read: you can use that to know when you reached the end of the file without knowing the size of the file to start with!
Here's an idea of how you can read an physical disk to a file, without knowing much about the disk device, using Delphi's TFileStream:
var DiskStream, DestinationStream:TFileStream;
Buff:array[0..512-1] of Byte;
BuffRead:Integer;
begin
// Open the disk for reading
DiskStream := TFileStream.Create('\\.\PhysicalDrive0', fmOpenRead);
try
// Create the file
DestinationStream := TFileStream.Create('D:\Something.IMG', fmCreate);
try
// Read & write in a loop; This is where all the work's done:
BuffRead := DiskStream.Read(Buff, SizeOf(Buff));
while BuffRead > 0 do
begin
DestinationStream.Write(Buff, BuffRead);
BuffRead := DiskStream.Read(Buff, SizeOf(Buff));
end;
finally DestinationStream.Free;
end;
finally DiskStream.Free;
end;
end;
You can obviously do something similar the other way around, reading from a file and writing to disk. Before writing that code I actually attempted doing it your way (getting the file size, etc), and immediately ran into problems! Apparently Windows doesn't know the exact size of the "file", not unless you read from it.
Problems with disks opened as files
For all my testing I used this simple code as the base:
var F: TFileStream;
begin
F := TFileStream.Create('\\.\PhysicalDrive0', fmOpenRead);
try
// Test code goes here...
finally F.Free;
end;
end;
The first (obvious) thing to try was:
ShowMessage(IntToStr(DiskStream.Size));
That fails. In the TFileStream implementation that depends on calling FileSeek, and FileSeek can't handle files larger then 2Gb. So I gave GetFileSize a try, using this code:
var RetSize, UpperWord:DWORD;
RetSize := GetFileSize(F.Handle, #UpperWord);
ShowMessage(IntToStr(UpperWord) + ' / ' + IntToStr(RetSize));
That also fails, even those it should be perfectly capable of returning file size as an 64 bit number! Next I tried using the SetFilePointer API, because that's also supposed to handle 64bit numbers. I thought I'd simply seek to the end of the file and look at the result, using this code:
var RetPos, UpperWord:DWORD;
UpperWord := 0;
RetPos := SetFilePos(F.Handle, 0, #UpperWord, FILE_END);
ShowMessage(IntToStr(UpperWord) + ' / ' + IntToStr(RetPos));
This code also fails! And now I'm thinking, why did the first code work? Apparently reading block-by-block works just fine and Windows knows exactly when to stop reading!! So I thought maybe there's a problem with the implementation of the 64 bit file handling routines, let's try seeking to end of the file in small increments; When we get an error seeking we know we reached the end we'll stop:
var PrevUpWord, PrevPos: DWORD;
UpWord, Pos: DWORD;
UpWord := 0;
Pos := SetFilePointer(F.Handle, 1024, #UpWord, FILE_CURRENT); // Advance the pointer 512 bytes from it's current position
while (UpWord <> PrevUpWord) or (Pos <> PrevPos) do
begin
PrevUpWord := UpWord;
PrevPos := Pos;
UpWord := 0;
Pos := SetFilePointer(F.Handle, 1024, #UpWord, FILE_CURRENT);
end;
When trying this code I had a surprise: It doesn't stop at the of the file, it just goes on and on, for ever. It never fails. To be perfectly honest I'm not sure it's supposed to ever fail... It's probably not supposed to fail. Anyway, doing a READ in that loop fails when we're past the end of file so we can use a VERY hacky mixed approach to handle this situation.
Ready-made routines that work around the problem
Here's the ready-made routine that gets the size of the physical disk opened as a file, even when GetFileSize fails, and SetFilePointer with FILE_END fails. Pass it an opened TFileStream and it will return the size as an Int64:
function Hacky_GetStreamSize(F: TFileStream): Int64;
var Step:DWORD;
StartPos: Int64;
StartPos_DWORD: packed array [0..1] of DWORD absolute StartPos;
KnownGoodPosition: Int64;
KGP_DWORD: packed array [0..1] of DWORD absolute KnownGoodPosition;
Dummy:DWORD;
Block:array[0..512-1] of Byte;
begin
// Get starting pointer position
StartPos := 0;
StartPos_DWORD[0] := SetFilePointer(F.Handle, 0, #StartPos_DWORD[1], FILE_CURRENT);
try
// Move file pointer to the first byte
SetFilePointer(F.Handle, 0, nil, FILE_BEGIN);
// Init
KnownGoodPosition := 0;
Step := 1024 * 1024 * 1024; // Initial step will be 1Gb
while Step > 512 do
begin
// Try to move
Dummy := 0;
SetFilePointer(F.Handle, Step, #Dummy, FILE_CURRENT);
// Test: Try to read!
if F.Read(Block, 512) = 512 then
begin
// Ok! Save the last known good position
KGP_DWORD[1] := 0;
KGP_DWORD[0] := SetFilePointer(F.Handle, 0, #KGP_DWORD[1], FILE_CURRENT);
end
else
begin
// Read failed! Move back to the last known good position and make Step smaller
SetFilePointer(F.Handle, KGP_DWORD[0], #KGP_DWORD[1], FILE_BEGIN);
Step := Step div 4; // it's optimal to devide by 4
end;
end;
// From here on we'll use 512 byte steps until we can't read any more
SetFilePointer(F.Handle, KGP_DWORD[0], #KGP_DWORD[1], FILE_BEGIN);
while F.Read(Block, 512) = 512 do
KnownGoodPosition := KnownGoodPosition + 512;
// Done!
Result := KnownGoodPosition;
finally
// Move file pointer back to starting position
SetFilePointer(F.Handle, StartPos_DWORD[0], #StartPos_DWORD[1], FILE_BEGIN);
end;
end;
To be complete, here are two routines that may be used to set and get the file pointer using Int64 for positioning:
function Hacky_SetStreamPos(F: TFileStream; Pos: Int64):Int64;
var aPos:Int64;
DWA:packed array[0..1] of DWORD absolute aPos;
const INVALID_SET_FILE_POINTER = $FFFFFFFF;
begin
aPos := Pos;
DWA[0] := SetFilePointer(F.Handle, DWA[0], #DWA[1], FILE_BEGIN);
if (DWA[0] = INVALID_SET_FILE_POINTER) and (GetLastError <> NO_ERROR) then
RaiseLastOSError;
Result := aPos;
end;
function Hacky_GetStreamPos(F: TFileStream): Int64;
var Pos:Int64;
DWA:packed array[0..1] of DWORD absolute Pos;
begin
Pos := 0;
DWA[0] := SetFilePointer(F.Handle, 0, #DWA[1], FILE_CURRENT);
Result := Pos;
end;
Last notes
The 3 routines I'm providing take as a parameter an TFileStream, because that's what I use for file reading and writing. They obviously only use TFileStream.Handle, so the parameter can simply be replaced with an file handle: the functionality would stay the same.
I know this thread is old, but...
One small suggestion - if you use the Windows DeviceIoControl(...) function you can get Drive Geometry and/or Partition Information, and use them to get the total size/length of the opened drive or partition. No more messing around with incrementally seeking to the end of the device.
Those IOCTLs can also be used to give you the correct volume sector size, and you could use that instead of defaulting to 512 everywhere.
Very very useful. But I got a problem for disks greater then 4 GB.
I solved replacing:
// Ok! Save the last known good position
KGP_DWORD[1] := 0;
KGP_DWORD[0] := SetFilePointer(F.Handle, 0, #KGP_DWORD[1], FILE_CURRENT);
with the following:
// Ok! Save the last known good position
KnownGoodPosition := KnownGoodPosition + Step;
Many thanks again...
And many thanks also to James R. Twine. I followed the advice of using IOCTL_DISK_GET_DRIVE_GEOMETRY_EX and got disk dimension with no problem and no strange workaround.
Here is the code:
TDISK_GEOMETRY = record
Cylinders : Int64; //LargeInteger
MediaType : DWORD; //MEDIA_TYPE
TracksPerCylinder: DWORD ;
SectorsPerTrack: DWORD ;
BytesPerSector : DWORD ;
end;
TDISK_GEOMETRY_EX = record
Geometry: TDISK_GEOMETRY ;
DiskSize: Int64; //LARGE_INTEGER ;
Data : array[1..1000] of byte; // unknown length
end;
function get_disk_size(handle: thandle): int64;
var
BytesReturned: DWORD;
DISK_GEOMETRY_EX : TDISK_GEOMETRY_EX;
begin
result := 0;
if DeviceIOControl(handle,IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
nil,0,#DISK_GEOMETRY_EX, sizeof(TDISK_GEOMETRY_EX),BytesReturned,nil)
then result := DISK_GEOMETRY_EX.DiskSize;
end;

Enumerate possible set values in Delphi

I have a calculation algorithm in Delphi with a number of different options, and I need to try every combination of options to find an optimal solution.
TMyOption = (option1, option2, option3, option4);
TMyOptions = set of TMyOption;
I wondered about using an Integer loop to enumerate them:
for EnumerationInteger := 0 to 15 do begin
Options := TMyOptions(EnumerationInteger);
end;
This does not compile. What I was wondering was if there was any fairly simple method to convert from Integer to Set (most questions on the Web try to go the other way, from Set to Integer), and if so what is it?
Another possibility is to just use the Integer as a bit-field:
C_Option1 = 1;
C_Option2 = 2;
C_Option3 = 4;
C_Option4 = 8;
and then test membership with a bitwise and:
if (Options and C_Option2) > 0 then begin
...
end;
I've tried this, and it works, but it feels like working with sets would be more natural and use the type system better (even though I'm going outside the said type system to enumerate the sets).
Is there a better/safer way to enumerate all possible set combinations than enumerating the underlying integer representation?
Notes:
I know that the integer values of a set are not guaranteed in theory (though I suspect they are in practice if you don't play with the enumeration numbering).
There could be more than four options (yes, I know that it grows exponentially and if there are too many options the algorithm could take forever).
I know this question is quite old, but this is my preference since it's simple and natural to me :
function NumericToMyOptions(n: integer): TMyOptions;
var
Op: TMyOption;
begin
Result:= [];
for Op:= Low(TMyOption) to High(TMyOption) do
if n and (1 shl ord(Op)) > 0 then Include(Result, Op);
end;
Try
var EnumerationByte: Byte;
...
for EnumerationByte := 0 to 15 do begin
Options := TMyOptions(EnumerationByte);
end;
Your code does not compile because your enumeration (TMyOption) have less than 8 values, and Delphi utilize the minimum possible size (in bytes) for sets. Thus, a byte variable will work for you.
If you have a set with more than 8 but less than 16 possible elements, a Word will work (and not an integer).
For more than 16 but less than 32 a DWord variable and typecast.
For more than 32 possible elements, I think a better approach is to use an array of bytes or something like that.
500 - Internal Server Error's answer is probably the most simple.
Another approach that would less likely to break with changes to the number of options would be to declare an array of boolean, and switch them on/off. This is slower than working with pure integers though. The main advantage, you won't need to change the integer type you use, and you can use it if you have more than 32 options.
procedure DoSomething
var BoolFlags : Array[TOption] of Boolean;
I: TOption;
function GetNextFlagSet(var Bools : Array of Boolean) : Boolean;
var idx, I : Integer;
begin
idx := 0;
while Bools[idx] and (idx <= High(Bools)) do Inc(idx);
Result := idx <= High(Bools);
if Result then
for I := 0 to idx do
Bools[I] := not Bools[I];
end;
begin
for I := Low(BoolFlags) to High(BoolFlags) do BoolFlags[i] := False;
repeat
if BoolFlags[Option1] then
[...]
until not GetNextFlagSet(BoolFlags);
end;
Casting from an Integer to a Set is not possible, but Tondrej once wrote a blog article on SetToString and StringToSet that exposes what you want in the SetOrdValue method:
uses
TypInfo;
procedure SetOrdValue(Info: PTypeInfo; var SetParam; Value: Integer);
begin
case GetTypeData(Info)^.OrdType of
otSByte, otUByte:
Byte(SetParam) := Value;
otSWord, otUWord:
Word(SetParam) := Value;
otSLong, otULong:
Integer(SetParam) := Value;
end;
end;
Your code then would become this:
for EnumerationInteger := 0 to 15 do begin
SetOrdValue(TypeInfo(TMyOptions), Options, EnumerationInteger);
end;
--jeroen
The problem is that you are trying to cast to the set type instead of the enumerated type. You can cast between integer and enumerated because both are ordinal types, but you can't cast to a set because they use bitfiels as you already noted. If you use:
for EnumerationInteger := 0 to 15 do begin
Option := TMyOption(EnumerationInteger);
end;
it would work, although is not what you want.
I had this same problem a few months ago and came to the conclusion that you can't enumerate the contents of a set in Delphi (at least in Delphi 7) because the language doesn't define such operation on a set.
Edit: It seems that you can even in D7, see coments to this answer.

Resources