21
votes

I copied some Delphi code from one project to another, and found that it doesn't compile in the new project, though it did in the old one. The code looks something like this:

procedure TForm1.CalculateGP(..)
const
   Price : money = 0;
begin
   ...
   Price := 1.0;
   ...
end;

So in the new project, Delphi complains that "left side cannot be assigned to" - understandable! But this code compiles in the old project. So my question is, why? Is there a compiler switch to allow consts to be reassigned? How does that even work? I thought consts were replaced by their values at compile time?

4

4 Answers

29
votes

You need to turn assignable typed constants on. Project -> Options -> Compiler -> Assignable typed Constants

Also you can add {$J+} or {$WRITEABLECONST ON} to the pas file, which is probably better, since it'll work even if you move the file to another project.

27
votes

Type-inferred constants can only be scalar values - i.e. things like integers, doubles, etc. For these kinds of constants, the compiler does indeed replace the constant's symbol with the constant's value whenever it meets them in expressions.

Typed constants, on the other hand, can be structured values - arrays and records. These guys need actual storage in the executable - i.e. they need to have storage allocated for them such that, when the OS loads the executable, the value of the typed constant is physically contained at some location in memory.

To explain why, historically, typed constants in early Delphi and its predecessor, Turbo Pascal, are writable (and thus essentially initialized global variables), we need to go back to the days of DOS.

DOS runs in real-mode, in x86 terms. This means that programs have direct access to physical memory without any MMU doing virtual-physical mappings. When programs have direct access to memory, no memory protection is in effect. In other words, if there is memory at any given address, it is both readable and writable in real-mode.

So, in a Turbo Pascal program for DOS with a typed constant, whose value is allocated at an address in memory at runtime, that typed constant will be writable. There is no hardware MMU getting in the way and preventing the program from writing to it. Similarly, because Pascal has no notion of 'const'ness that C++ has, there is nothing in the type system to stop you. A lot of people took advantage of this, since Turbo Pascal and Delphi did not at that time have initialized global variables as a feature.

Moving on to Windows, there is a layer between memory addresses and physical addresses: the memory management unit. This chip takes the page index (a shifted mask) of the memory address you're trying to access, and looks up the attributes of this page in its page table. These attributes include readable, writable, and for modern x86 chips, non-executable flags. With this support, it's possible to mark sections of the .EXE or .DLL with attributes such that when the Windows loader loads the executable image into memory, it assigns appropriate page attributes for memory pages that map to disk pages within these sections.

When the 32-bit Windows version of the Delphi compiler came around, it thus made sense to make const-like things really const, as the OS also has this feature.

11
votes
  1. Why: Because in previous versions of Delphi the typed constants were assignable by default to preserve compatibility with older versions where they were always writable (Delphi 1 up to early Pascal).
    The default has now been changed to make constants really constant…

  2. Compiler switch: {$J+} or {$J-} {$WRITEABLECONST ON} or {$WRITEABLECONST OFF}
    Or in the project options for the compiler: check assignable typed Constants

  3. How it works: If the compiler can compute the value at compile time, it replaces the const by its value everywhere in the code, otherwise it holds a pointer to a memory area holding the value, which can be made writeable or not.
  4. see 3.
2
votes

Like Barry said, people took advantage of consts; One of the ways this was used, was for keeping track of singleton instances. If you look at a classic singleton implementation, you would see this :

  // Example implementation of the Singleton pattern.
  TSingleton = class(TObject)
  protected
    constructor CreateInstance; virtual;
    class function AccessInstance(Request: Integer): TSingleton;
  public
    constructor Create; virtual;
    destructor Destroy; override;
    class function Instance: TSingleton;
    class procedure ReleaseInstance;
  end;

constructor TSingleton.Create;
begin
  inherited Create;

  raise Exception.CreateFmt('Access class %s through Instance only', [ClassName]);
end;

constructor TSingleton.CreateInstance;
begin
  inherited Create;

  // Do whatever you would normally place in Create, here.
end;

destructor TSingleton.Destroy;
begin
  // Do normal destruction here

  if AccessInstance(0) = Self then
    AccessInstance(2);

  inherited Destroy;
end;

{$WRITEABLECONST ON}
class function TSingleton.AccessInstance(Request: Integer): TSingleton;
const
  FInstance: TSingleton = nil;
begin
  case Request of
    0 : ;
    1 : if not Assigned(FInstance) then
          FInstance := CreateInstance;
    2 : FInstance := nil;
  else
    raise Exception.CreateFmt('Illegal request %d in AccessInstance', [Request]);
  end;
  Result := FInstance;
end;
{$IFNDEF WRITEABLECONST_ON}
  {$WRITEABLECONST OFF}
{$ENDIF}

class function TSingleton.Instance: TSingleton;
begin
  Result := AccessInstance(1);
end;

class procedure TSingleton.ReleaseInstance;
begin
  AccessInstance(0).Free;
end;