How to get size of adjacent record’s fields correctly?

Suppose we have two record types TRecA and TRecB declared as shown below; in a sense the TRecB type is ‘inherited’ from TRecA.

The goal is to write a general method which clears all TRecB fields which are not ‘inherited’ from TRecA. The code below propose 3 candidate solutions TRecB.ClearX:

    type
      PRecA = ^TRecA;
      TRecA = record
        FieldA1: LongWord;
        FieldA2: Byte;
      end;
    
      PRecB = ^TRecB;
      TRecB = record
        FieldA1: LongWord;
        FieldA2: Byte;
    
        FieldB1: Word;
        FieldB2: LongInt;
        FieldB3: Byte;
        Sentinel: record end;
    
        procedure Clear1;
        procedure Clear2;
        procedure Clear3;
      end;
    
    { TRecB }
    
    procedure TRecB.Clear1;
    begin
      FillChar(FieldB1, SizeOf(TRecB) - SizeOf(TRecA), 0);
    end;
    
    procedure TRecB.Clear2;
    begin
      FillChar(FieldB1,
        SizeOf(TRecB) - Integer(@PRecB(nil)^.FieldB1), 0);
    end;
    
    procedure TRecB.Clear3;
    begin
      FillChar(FieldB1,
        Integer(@PRecB(nil)^.Sentinel) - Integer(@PRecB(nil)^.FieldB1), 0);
    end;

At a first glance all three ClearX methods do the same but they are not. The Clear1 solution is simply wrong while Clear2 and Clear3 are different.

To understand why it is so one need to understand how types’ sizes and alignments are calculated.

Since the 80-bit Extended was kicked out from Delphi, every type’s size is multiple of the type’s alignment. TRecA type has alignment 4 (which is the LongWord type alignment, the biggest alignment among TRecA fields), and the size of the 2nd field is less than 4, so SizeOf(TRecA) = 8.

The alignment of Word type is 2, thus the offset of FieldB1 in TRecB type is 6. That is why Clear1 solution is wrong: it may work correctly for some sets of FieldBX fields, but generally it is incorrect.

Applying the alignment rules to TRecB type we obtain the next layout:

      TRecBLayout = record
        FieldA1: LongWord;
        FieldA2: Byte;
        Filler1: Byte;  
        FieldB1: Word;
        FieldB2: LongInt;
        FieldB3: Byte;
        Filler2: Byte;  
        Filler3: Byte;  
        Filler4: Byte;
      end;  

Filler bytes at the end are required to make TRecB size multiple of TRecB alignment (=4)

So SizeOf(TRecB) = 16, but OffsetOf(TRecB.Sentinel) = 13 (unfortunately Delphi compiler does not support OffsetOf() built-in). That is why Clear2 and Clear3 methods are different: only Clear3 does exactly what is asked – clears ‘non-inherited’ fields; Clear2 also clears filler bytes at the record’s end.

Advertisements

One thought on “How to get size of adjacent record’s fields correctly?

  1. Clearing also the filler bytes aswell is usually what you want because some code also takes these into account f.i. comparers from Generics.Defaults which simply do a dumb binary compare of the record memory. If you don’t clear the filler bytes also you might get wrong results comparing values that should be the same just because the memory in the filler bytes differs.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s