Can Identifiers be duplicated across cases of a variant record in FreePascal?
Asked Answered
B

3

7

Here's my problem: I want to create a record type where among the cases of a variant record, some, but not all, will have a certain field. According to the wiki, this is perfectly legal. And yet, when I tried to compile the following code:

program example;

{$mode objfpc}{$H+}

uses sysutils;

type
  maritalStates = (single, married, widowed, divorced);

  TPerson = record
    name: record
      first, middle, last: string;
    end;
    sex: (male, female);
    dob: TDateTime;
    case maritalStatus: maritalStates of
      single: ( );
      married, widowed: (marriageDate: TDateTime);
      divorced: (marriageDate, divorceDate: TDateTime;
        isFirstDivorce: boolean)      
  end;

var
  ExPerson: TPerson;

begin
ExPerson.name.first := 'John';
ExPerson.name.middle := 'Bob';
ExPerson.name.last := 'Smith';
ExPerson.sex := male;
ExPerson.dob := StrToDate('05/05/1990');
ExPerson.maritalStatus := married;
ExPerson.marriageDate := StrToDate('04/01/2015');

end.

the compilation fails with the following error:

$ fpc ex.pas
Free Pascal Compiler version 3.0.0 [2016/02/14] for x86_64
Copyright (c) 1993-2015 by Florian Klaempfl and others
Target OS: Win64 for x64
Compiling ex.pas
ex.pas(19,18) Error: Duplicate identifier "marriageDate"
ex.pas(21,3) Error: Duplicate identifier "marriageDate"
ex.pas(35,4) Fatal: There were 2 errors compiling module, stopping
Fatal: Compilation aborted
Error: C:\lazarus\fpc\3.0.0\bin\x86_64-win64\ppcx64.exe returned an error exitcode

Is the wiki simply wrong, or am I missing something here? Is there any way to achieve this effect I want?

Besprent answered 21/4, 2016 at 6:24 Comment(3)
Awesome! That worked! Do you want to add this as answer so I can marked the question solved, or do you want me to answer it?Besprent
Wiki is writable for everybody that registers, and is not authoritative. There are no FPC people in the list of contributors to that page. It might also be done by sb using a different mode (.e.g ISO or Macpas).Countermove
FWIW, ISTM that people can be divorced and widowed or married. Or all of these. They can also be divorced and single. <g>Moulton
A
3

Very interesting question. I was sure this is possible. If You modify Your code to:

..
married, widowed, divorced: (marriageDate: TDateTime);
divorced: (divorceDate: TDateTime; isFirstDivorce: boolean)
..

it works, but it is not the result you intend to have. Since marriageDate and the divorceDate overlay each other (as mentioned in the comments!)

enter image description here

This picture is taken from "Pascal users manual (4th edition)" and as you can see the variant parts have the same memory location.

According to Pascal users manual (4th edition) and to the book "Turbo Pascal ISBN 3-89011-060-6 the described record declaration on your quoted wiki is not valid!

  1. All field names must be distinct - even if they occur in different variants.
  2. If a variant is empty (i.e., has no fields), the form is: C:()
  3. A field list can have only one variant part and it must follow the fixed part of the record.
  4. A variant may itself contain a variant part; hence variant parts can be nested.
  5. The scope of enumerated type constant identifiers that are introduced in a record type extends over the enclosing block.

Point 1 is the relevant one here! The book "Turbo Pascal" the suggested solution is to use a unique prefix for field names that occur multiple times.

In Your case Your could would look like:

TPerson = record
    name: record
      first, middle, last: string;
    end;
    sex: (male, female);
    dob: TDateTime;
    case maritalStatus: maritalStates of
      single: ( );
      married, widowed: (marMarriageDate: TDateTime);
      divorced: (divMarriageDate, divorceDate: TDateTime;
        isFirstDivorce: boolean)      
  end;

The other solution would be to define married, devorced ... as a record type.

..
married       : (m: TMarried);
divorced      : (d: TDivorced);
..
Abhenry answered 21/4, 2016 at 8:37 Comment(4)
In this case marriageDate and the divorceDate overlay each other. If you assign to marriageDate, you'll read the same date from divorceDate. IOW, they occupy the same memory location.Base
As Allen said, this is not the solution. But you can probably declare the single parts as record types beforehand, and use these as the types of parts of the variant.Moulton
You're right. I will look araund for a solution Tomorrow and correct the answerAbhenry
I still wonder why everyone tries to make it so complicated? marMarriageDate and vidMarriagedate will overlap, so why not just one marriageDate that is not variant? The fact that single does not need a marriage date doesn't matter. Just don't use it. And the record size will be exactly the same anyway.Moulton
O
1

This seems to work

program example;

{$mode objfpc}{$H+}

uses sysutils;

type
  TMarried          = record
                        marriageDate  : TDateTime
                      end;

  TDivorced         = record
                        marriageDate  : TDateTime;
                        divorceDate   : TDateTime;
                        isFirstDivorce: boolean
                      end;

  TWidowed          = TMarried;

  maritalStates = (single, married, widowed, divorced);

  TPerson = record
    name: record
      first, middle, last: string;
    end;
    sex: (male, female);
    dob: TDateTime;
    case maritalStatus: maritalStates of
      single        : ();
      married       : (m: TMarried);
      widowed       : (w: TWidowed);
      divorced      : (d: TDivorced);
  end;

var ExPerson: TPerson;

begin
  with ExPerson do
  begin
    name.first := 'John';
    name.middle := 'Bob';
    name.last := 'Smith';
    sex := male;
    dob := StrToDate('05/05/1990');
    maritalStatus := married;
    m.marriageDate := StrToDate('04/01/2015');
  end;
end.

EDIT: You could also define the records inline but the above is clearer, I think. Here's the alternative way:

program example;

{$mode objfpc}{$H+}

uses sysutils;

type
  maritalStates = (single, married, widowed, divorced);

  TPerson = record
    name: record
      first, middle, last: string;
    end;
    sex: (male, female);
    dob: TDateTime;
    case maritalStatus: maritalStates of
      single   : ();
      married  : (m: record marriageDate: TDateTime end);
      widowed  : (w: record marriageDate: TDateTime end);
      divorced : (d: record
                       marriageDate  : TDateTime;
                       divorceDate   : TDateTime;
                       isFirstDivorce: boolean
                     end)
  end;

var ExPerson: TPerson;

begin
  with ExPerson do
  begin
    name.first  := 'John';
    name.middle := 'Bob';
    name.last   := 'Smith';
    sex := male;
    dob := StrToDate('05/05/1990');
    maritalStatus  := married;
    m.marriageDate := StrToDate('04/01/2015');
  end;
end.
Omaomaha answered 21/4, 2016 at 19:45 Comment(12)
Yes, it works, but it is overly complicated. Just get rid of the variant part and put all fields after each other in the record. That solves all problems, is much simpler and has no alignment issues.Moulton
Putting all fields in sequence inflates the record. This case was just an example. It could be much more involved. Consider this record saved to a file multiplied by 500 million people. Even a single byte more than needed will have significant effect on your file size. There was a reason variant records were invented. Also, a record is guaranteed to be stored as a contiguous chunk of memory, so no alignment issues. (If it weren't you couldn't possibly use it correctly in some cases, e.g., when the variant is meant to simply give a different interpretation of the same memory locations.)Omaomaha
Putting all fields in sequence does not "inflate" the record. The variant record will be big enough to hold all fields, so the largest variant part determines the size of the record, together with the fixed part. That will be exactly the same size as the non-variant record. Try it. Of course you should only have one marriageDate, one divorcedDate etc.Moulton
I measured your version with nested and differently named records in a variant part, and my plain straightforward record, and they are both exactly the same size: 56 bytes. Their layout is the same, except that to access your fields, you must prefix with with m., w. or d.. Note that all marriageDate fields in your record overlap anyway. So again: why so complicated?Moulton
FWIW, records can certainly have alignment issues, in FreePascal. Fields are aligned on their natural boundaries, so a 4 byte type on a 4 byte boundary, etc. The alignment of the entire record even depends on the size of the largest type in the record, and also the size depends on that.Moulton
@RudyVelthuis I'm afraid you don't have a clear understanding of variant records. See these two examples (gist.github.com/tonypdmtr/cae849750376d5cc7e2028e0835e385a), and try them out to see what data files they create. Compare sizes and contents.Omaomaha
I have a very clear understand of variant records (am programming in Pascal since the early eighties) and I know that items in the variant parts overlap, so yes, your github example records have different sizes. But take a look at my answer and tell me which of the records is larger: the one your propose or mine, without variant parts. I think it is you who doesn't fully understand the layout of variant records. Trust me if I tell you that there is no difference in layout. My marriageDate covers all yours, since they all overlap. and the extra fields do not have to be variant either. Try it.Moulton
I'll write up an example with both layouts: your and mine, and then I'll post the offsets of the fields. You'll see what I mean. But probably not today.Moulton
I just udpated my answer. Take a look at it and try it out yourself. As you can see, w.marriageDate, m.marriageDate and d.marriageDatein your record have all the same offset, which is the same as the offset of marriageDate in my non-variant record. And the following fields have, of course, the same higher offsets, exactly the same as in my non-variant record. Variant records certainly make sense, but not in this case.Moulton
Hmm, final try from me: THIS CASE WAS JUST AN EXAMPLE. We're not asked if a variant record makes sense in this particular case but how to deal with the issue of duplicate names. Your attempt to prove that variant records make no sense in this case is not the issue. I could give infinite examples, too. Obviously, when common fields are present in all variant parts, these have to move out to the common part. But when you have to use variant records, how do you solve the name issue... Anyway, I think we can put it to rest now.Omaomaha
I am not so sure that this case was an example, but even if it were, it clearly shows that it doesn't always make sense to use a variant record when a simple "flat" record will do. If someone asks me "should I use a pocket or a bread knife to change a tyre", I will tell them that there are better tools.Moulton
I could point out a few more errors in your reasoning, but I won't.Moulton
M
0

What Baltasar proposes will compile, but not do what you want. marriageDate and divorceDate will overlap and writing to one of them will also modify the other, since they are simply at the same address.

But in this case, there is no good reason for a variant record at all.

Why not simply:

type
  maritalStates = (single, married, widowed, divorced);

  TPerson = record
    name: record
      first, 
      middle, 
      last: string;
    end;
    sex: (male, female);
    dob: TDateTime;
    maritalStatus: maritalStates; // single, married, widowed, divorced
    marriageDate: TDateTime;      // married, widowed, divorced   
    divorceDate   : TDateTime;    // divorced
    isFirstDivorce: boolean;      // divorced
  end;

The usage and layout is exactly what you need. If a field does not apply (e.g. marriageDate for a single, or divorceDate for a married), you simply don't use it.

That is the same as with a variant record. There you also only set the fields that apply. Note that the compiler or runtime do not prevent you from writing to the wrong field of a variant record anyway, i.e. in a variant record, if the status is single, you can still write to or read from divorceDate, even if that makes no sense.

If you want to distinguish several different setups, simply do that in comments, and forget the variant record, you don't need it here. Now you can do:

var
  P: TPerson;
begin
  P.name.first := 'Bob';
  P.name.middle := 'The';
  P.name.last := 'Builder';
  P.sex := male;
  P.dob := StrToDate('05/05/1980');
  P.maritalStatus := divorced;
  P.marriageDate := StrToDate('04/01/2013');
  P.divorceDate := StrToDate('04/02/2016');
  P.isFirstDivorce := True;

  // etc...

Update

Just to show that there is absolutely no need to make this record variant,

I will post my Project62.dpr, which shows exactly the same offsets for corresponding fields and the same record sizes:

program Project62;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  maritalStates = (single, married, widowed, divorced);
  tsex = (male, female);

  // No variant part
  PPerson = ^TPerson;
  TPerson = record
    name: record
      first,
      middle,
      last: string;
    end;
    sex: tsex;
    dob: TDateTime;
    maritalStatus: maritalStates; // single, married, widowed, divorced
    marriageDate: TDateTime;      // married, widowed, divorced
    divorceDate   : TDateTime;    // divorced
    isFirstDivorceDate: boolean;      // divorced
  end;

  // Variant part like tonypdmtr's record
  PPerson2 = ^TPerson2;
  TPerson2 = record
    name: record
      first,
      middle,
      last: string;
    end;
    sex: tsex;
    dob: TDateTime;
    case maritalStatus: maritalStates of
      single:   ();
      widowed:  (w: record marriageDate: TDateTime; end); // overlaps with m.marriageDate and d.marriageDate
      married:  (m: record marriageDate: TDateTime; end); // overlaps with w.marriageDate and d.marriageDate
      divorced: (d: record
                      marriageDate: TDateTime;            // overlaps with w.marriageDate and m.marriageDate
                      divorceDate: TDateTime;             // same offset as in my non-variant version
                      isFirstDivorceDate: Boolean         // same offset as in my non-variant version
                    end);
  end;

begin
  try
    Writeln('TPerson:  size = ', Sizeof(TPerson));
    Writeln('TPerson.maritalStatus:         offset = ', NativeUInt(@PPerson(nil)^.maritalStatus));
    Writeln('TPerson.marriageDate:          offset = ', NativeUInt(@PPerson(nil)^.marriageDate));
    Writeln('TPerson.divorceDate:           offset = ', NativeUInt(@PPerson(nil)^.divorceDate));
    Writeln('TPerson.isFirstDivorceDate:    offset = ', NativeUInt(@PPerson(nil)^.isFirstDivorceDate));
    Writeln;
    Writeln('TPerson2:  size = ', Sizeof(TPerson2));
    Writeln('TPerson2.maritalStatus:        offset = ', NativeUInt(@PPerson2(nil)^.maritalStatus));
    Writeln('TPerson2.w.marriageDate:       offset = ', NativeUInt(@PPerson2(nil)^.w.marriageDate));
    Writeln('TPerson2.m.marriageDate:       offset = ', NativeUInt(@PPerson2(nil)^.m.marriageDate));
    Writeln('TPerson2.d.marriageDate:       offset = ', NativeUInt(@PPerson2(nil)^.d.marriageDate));
    Writeln('TPerson2.d.divorceDate:        offset = ', NativeUInt(@PPerson2(nil)^.d.divorceDate));
    Writeln('TPerson2.d.isFirstDivorceDate: offset = ', NativeUInt(@PPerson2(nil)^.d.isFirstDivorceDate));
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;
end.

Output (on Windows):

TPerson:  size = 56
TPerson.maritalStatus:         offset = 24
TPerson.marriageDate:          offset = 32
TPerson.divorceDate:           offset = 40
TPerson.isFirstDivorceDate:    offset = 48

TPerson2:  size = 56
TPerson2.maritalStatus:        offset = 24
TPerson2.w.marriageDate:       offset = 32
TPerson2.m.marriageDate:       offset = 32
TPerson2.d.marriageDate:       offset = 32
TPerson2.d.divorceDate:        offset = 40
TPerson2.d.isFirstDivorceDate: offset = 48

The layout in 32 bit can be put in a simple diagram like so:

  00 TPerson: [name.first]          TPerson2: [name.first]
  04          [name.middle]                   [name.middle]
  08          [name.last]                     [name.last]
  12          [sex]                           [sex]
  16          [dob]                           [dob]
  24          [maritalStatus]                 [maritalStatus]
  32          [marriageDate]                  [w.marriageDate] [m.marriageDate] [d.marriageDate]
  40          [divorceDate]                                                     [d.divorceDate]
  48          [isFirstDivorceDate]                                              [d.isFirstDivorceDate]
Moulton answered 21/4, 2016 at 23:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.