How to work with 0-based strings in a backwards compatible way since Delphi XE5?
Asked Answered
C

6

20

I'm trying to convert my current Delphi 7 Win32 code to Delphi XE5 Android with minimal changes, so that my project can be cross-compiled to Win32 from a range of Delphi versions and Android from XE5.

Starting from XE5 there are breaking changes in language aimed at future. One of such changes is zero-based strings.

In older versions with 1-based strings the following code was correct:

function StripColor(aText: string): string;
begin
  for I := 1 to Length(aText) do

but now this is obviously not right. Suggested solution is to use:

for I := Low(aText) to High(aText) do

This way XE5 Win32 handles 1-based strings and XE5 Android handles 0-based strings right. However there's a problem - previous Delphi versions (e.g. XE2) output an error on such code:

E2198 Low cannot be applied to a long string
E2198 High cannot be applied to a long string

I have quite a lot of string manipulation code. My question is - how to modify and keep above code to be compileable in Delphi 7 Win32 and Delphi XE5 Android?

P.S. I know I can still disable ZEROBASEDSTRINGS define in XE5, but that is undesired solution since in XE6 this define will probably be gone and all strings will be forced to be 0-based.

Carrefour answered 21/10, 2013 at 6:56 Comment(10)
@David: Seriously, we all saw the charts about which Delphi versions are most popular. Delphi 7 is still near the top ;) Since I work on an open-source project I want to keep the code as much compatible as possible (including Lazarus).Carrefour
i believe for Delphi-7 you can write your own functions and use them transparently. but i don't have D7 and cannot check. function Low(const S: AnsiString): integer; overload; begin ... end; - You would need 4 functions: Low/High and AnsiString/WideString - and i hope that overload keyword would do the trick, so you would have both stock Low/High for arrays and your own Low/High for stringsKreager
@Arioch'The: yes, do post your suggestion as an answer.Jorgan
@LURD no, for Delphi 2009-XE5 he should just use built-in functions and avoid declaring his own onesKreager
@Arioch'The: I have tested and it seems Low/High can not be overloaded like so.Carrefour
Possible, but not practical, unfortunately. And it is also not practical to use ifdefs everywhere a string index is used. Now where does this leave us trying to write code that works with different compiler versions?Devlen
@LURD: See my answer, the last line.Carrefour
Since evidence shows that overloading of functions does not work, i suggest that topic starter would describe it in his question, as a method that looked promising but did not actually work. After that we would be able to clean comments/answers about overloading without risk that someone later would come out with that idea yet again.Kreager
I would upgrade the old code to zero-based, and use for i := 0 to Pred(Length(sText)) do. I prefer the scenic route.Benefit
@Sam: Old code needs to be compatible with older version of Delphi which use 1-based strings. Hence the question ;)Carrefour
U
2

This is rather a sum up of the two answers:

As pointed out by Remy Lebeau, ZEROBASEDSTRINGS is a per-block conditional. That means that the following code will not work as expected:

const
  s: string = 'test';

function StringLow(const aString: string): Integer; inline; // <-- inline does not help
begin
  {$IF CompilerVersion >= 24} 
  Result := Low(aString); // Delphi XE3 and up can use Low(s)
  {$ELSE}
  Result := 1;  // Delphi XE2 and below can't use Low(s), but don't have ZEROBASEDSTRINGS either
  {$ENDIF}
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  {$ZEROBASEDSTRINGS OFF}
  Memo1.Lines.Add(Low(s).ToString);        // 1
  Memo1.Lines.Add(StringLow(s).ToString);  // 1
  {$ZEROBASEDSTRINGS ON}
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  {$ZEROBASEDSTRINGS ON}
  Memo1.Lines.Add(Low(s).ToString);        // 0
  Memo1.Lines.Add(StringLow(s).ToString);  // 1  <-- Expected to be 0
  {$ZEROBASEDSTRINGS OFF}
end;

There are 2 possible solutions:

A. Every time there's string items access or iteration place an IFDEF around it, which is indeed a lot of clutter for the code, but will work properly irregardless of ZEROBASEDSTRINGS setting around it:

for I := {$IFDEF XE3UP}Low(aText){$ELSE}1{$ENDIF} to {$IFDEF XE3UP}High(aText){$ELSE}Length(aText){$ENDIF} do

B. Since the ZEROBASEDSTRINGS conditional is per-block it never gets spoiled by 3rd party code and if you don't change it in your code you are fine (above StringLow will work fine as long as the caller code has the same ZEROBASEDSTRINGS setting). Note that if target is mobile, you should not apply ZEROBASEDSTRINGS OFF globally in your code since RTL functions (e.g. TStringHelper) will return 0-based results because mobile RTL is compiled with ZEROBASEDSTRINGS ON.

On a side note - One might suggest to write an overloaded versions of Low/High for older versions of Delphi, but then Low(other type) (where type is array of something) stops working. It looks like since Low/High are not usual functions then can not be overloaded that simply.

TL;DR - Use custom StringLow and don't change ZEROBASEDSTRINGS in your code.

Underwater answered 21/10, 2013 at 6:57 Comment(0)
D
6

If you want to support versions that use one based strings then don't define ZEROBASEDSTRINGS. That's the purpose of that conditional.

There's no indication that I am aware of that the conditional will be removed any time soon. It was introduced in XE3 and has survived two subsequent releases. If Embarcadero remove it, none of their Win32 customers will not upgrade and they will go bust. Embarcadero have a track record of maintaining compatibility. You can still use TP objects and short strings. Expect this conditional to live as long as the desktop compiler does.

In fact, all the evidence points towards the mobile compilers retaining support for one based string indexing. All the utility string functions like Pos use one based indices, and will continue to do so. If Embarcadero really are going to remove support for one based string indexing, they'll be removing Pos too. I don't believe that is likely any time soon.

Taking your question at face value though it is trivial to write functions that return the low and high indices of a string. You just use an IFDEF on the compiler version.

function StrLow(const S: string): Integer; inline;
begin
  Result := {$IFDEF XE3UP}low(S){$ELSE}1{$ENDIF}
end;

function StrHigh(const S: string): Integer; inline;
begin
  Result := {$IFDEF XE3UP}high(S){$ELSE}Length(S){$ENDIF}
end;

Update

As Remy points out, the above code is no good. That's because ZEROBASEDSTRINGS is local and what counts is its state at the place where such functions would be used. In fact it's just not possible to implement these functions in a meaningful way.

So, I believe that for code that needs to be compiled using legacy compilers, as well as the mobile compilers, you have little choice but to disable. ZEROBASEDSTRINGS.

Discomfiture answered 21/10, 2013 at 7:10 Comment(12)
The thing is, mobile compilers are likely to have 0-based strings enforced since XE6. So to have the same code for mobile and Win32 there needs to be a way to handle 0-based and 1-based strings with the same code.Carrefour
Really. There's been an official statement to that effect? Do you have a link?Discomfiture
Official Embarcadero docs advice to rewrite 1-based code into 0-based for mobile platforms (docwiki.embarcadero.com/RADStudio/XE4/en/… 0-based strings section). I doubt they would have made 0-based just to keep it an option without plans to enforce it in the future.Carrefour
They also advise you not to use short strings or tp objets. In fact, didn't they get ported to mobile compilers?!! I see no evidence that that conditional will be removed imminently. I'd re-write the code when you need to. Likely that need will never arise.Discomfiture
@DavidHeffernan tp objects and short strings ? in Delphi/LLVM ? Maybe they were ported by some 3rd-party but not by EMBT. blog.synopse.info/post/2013/10/09/… blog.synopse.info/post/2013/05/11/…Kreager
Actually I was talking nonsense. These ancient features are not on the mobile compilers.Discomfiture
@DavidHeffernan: Also, keep in mind that ZEROBASEDSTRINGS is a per-block conditional, not a per-unit conditional or a per-project conditional, so your StrLow/High() approach is not guaranteed to work. Think about it. If the unit that contains the implementation of StrLow/High() has ZEROBASEDSTRINGS on, but a unit that calls StrLow/High() has ZEROBASEDSTRINGS off, then you will be returning the wrong values.Jorgan
@RemyLebeau even with the inline?Discomfiture
@DavidHeffernan: I don't know if it would work with inlining. Try it and see. But you do know that inlining is only a hint, right? The compiler is not obligated or guaranteed to actually inline the code if it chooses not to do so.Jorgan
@RemyLebeau Not got a compiler now to check. I know inline does not force it to happen. I had not twigged the issue of locality that you mentioned. So +1 to you.Discomfiture
@krom the inlining doesn't get around the issue?Discomfiture
@David: No, inlining does not help. See my answer. Basing on yours, but with additional info about never using ZEROBASEDSTRINGS ON/OFF in code - then it works right.Carrefour
J
3

All of the RTL's pre-existing functions (Pos(), Copy(), etc) are still (and will remain) 1-based for backwards compatibility. 0-based functionality is exposed via the new TStringHelper record helper that was introduced in XE3, which older code will not be using so nothing breaks.

The only real gotchas you have to watch out for are things like hard-coded indexes, such as your loop example. Unfortunately, without access to Low/High(String) in older Delphi versions, the only way to write such code in a portable way is to use IFDEFs, eg:

{$IFDEF CONDITIONALEXPRESSIONS}
  {$IF CompilerVersion >= 24}
    {$DEFINE XE3_OR_ABOVE}
  {$IFEND}
{$ENDIF}

function StripColor(aText: string): string;
begin
  for I := {$IFDEF XE3_OR_ABOVE}Low(aText){$ELSE}1{$ENDIF} to {$IFDEF XE3_OR_ABOVE}High(AText){$ELSE}Length(aText){$ENDIF} do
    DoSomething(aText, I);
end;

Or:

{$IFDEF CONDITIONALEXPRESSIONS}
  {$IF CompilerVersion >= 24}
    {$DEFINE XE3_OR_ABOVE}
  {$IFEND}
{$ENDIF}

function StripColor(aText: string): string;
begin
  for I := 1 to Length(aText) do
  begin
    DoSomething(aText, I{$IFDEF XE3_OR_ABOVE}-(1-Low(AText)){$ENDIF});
  end;
end;

Conditional Expressions were introduced in Delphi 6, so if you don't need to support version earlier than Delphi 7, and don't need to support other compilers like FreePascal, then you can omit the {$IFDEF CONDITIONALEXPRESSIONS} check.

Jorgan answered 21/10, 2013 at 19:10 Comment(1)
I didn't say it would be pretty, only functional. And I hadn't thought of overloading Low/High() at the time.Jorgan
U
2

This is rather a sum up of the two answers:

As pointed out by Remy Lebeau, ZEROBASEDSTRINGS is a per-block conditional. That means that the following code will not work as expected:

const
  s: string = 'test';

function StringLow(const aString: string): Integer; inline; // <-- inline does not help
begin
  {$IF CompilerVersion >= 24} 
  Result := Low(aString); // Delphi XE3 and up can use Low(s)
  {$ELSE}
  Result := 1;  // Delphi XE2 and below can't use Low(s), but don't have ZEROBASEDSTRINGS either
  {$ENDIF}
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  {$ZEROBASEDSTRINGS OFF}
  Memo1.Lines.Add(Low(s).ToString);        // 1
  Memo1.Lines.Add(StringLow(s).ToString);  // 1
  {$ZEROBASEDSTRINGS ON}
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  {$ZEROBASEDSTRINGS ON}
  Memo1.Lines.Add(Low(s).ToString);        // 0
  Memo1.Lines.Add(StringLow(s).ToString);  // 1  <-- Expected to be 0
  {$ZEROBASEDSTRINGS OFF}
end;

There are 2 possible solutions:

A. Every time there's string items access or iteration place an IFDEF around it, which is indeed a lot of clutter for the code, but will work properly irregardless of ZEROBASEDSTRINGS setting around it:

for I := {$IFDEF XE3UP}Low(aText){$ELSE}1{$ENDIF} to {$IFDEF XE3UP}High(aText){$ELSE}Length(aText){$ENDIF} do

B. Since the ZEROBASEDSTRINGS conditional is per-block it never gets spoiled by 3rd party code and if you don't change it in your code you are fine (above StringLow will work fine as long as the caller code has the same ZEROBASEDSTRINGS setting). Note that if target is mobile, you should not apply ZEROBASEDSTRINGS OFF globally in your code since RTL functions (e.g. TStringHelper) will return 0-based results because mobile RTL is compiled with ZEROBASEDSTRINGS ON.

On a side note - One might suggest to write an overloaded versions of Low/High for older versions of Delphi, but then Low(other type) (where type is array of something) stops working. It looks like since Low/High are not usual functions then can not be overloaded that simply.

TL;DR - Use custom StringLow and don't change ZEROBASEDSTRINGS in your code.

Underwater answered 21/10, 2013 at 6:57 Comment(0)
H
2

How about defining this as an inc file? Put additional ifdefs depending on what Delphi versions you want to support. Since this code is only for versions before the ZBS to make it possible to use Low and High on strings it will not run into the problem with the ZEROBASEDSTRINGS define only being local.

You can include this code locally (as nested routines) then which reduces the risk of colliding with System.Low and System.High.

{$IF CompilerVersion < 24}
function Low(const s: string): Integer; inline;
begin
  Result := 1;
end;

function High(const s: string): Integer; inline;
begin
  Result := Length(s);
end;
{$IFEND}
Hipped answered 8/5, 2014 at 13:34 Comment(0)
S
1

As LU RD told above Low and High functions for string were only introduced in XE3. So how can you use functions in earlier Delphi verions, that are missed? Just the same way as always do - if the function is missed - go and write it!

You should only activate those compatibility additions for Delphi beyond XE3 version, using conditional compilation. One way is described in other answers, using >= comparison. Another usual way would be reusing jedi.inc definitions file.

Then for earlier Delphi versions you would add your own implementations of those, like

function Low(const S: AnsiString): integer; overload;

Pay attention to the overload specifier - it is what would make the trick possible, don't forget it!

You would have to write 4 functions for Delphi 7 till 2007, covering combinations of Low/High fn name and AnsiString/WideString data type.

For Delphi 2009 till XE2 you would have to add two more functions for UnicodeString datatype.

And also mark those function inline for those Delphi versions, that support it (this is where jedi.inc comes handy again.

Hopefully you don't need supprot for UTF8String, but if you do - you know what to do about it now (if compiler would manage to tell it from AnsiString when overloading...)

Scud answered 22/10, 2013 at 8:9 Comment(8)
I have tested that and Low(type) (where type is array of something) stopped working. It looks like Low/High are not usual functions and can not be overloaded that simply.Carrefour
@KromStern, yes indeed. The overload hides other intrinsic Low/High functions. They have to be qualified with System.Low. Makes everything a bit convoluted.Devlen
:-( That is bad.... You can use System.Low for arrya, etc - but that approach would take a lot of work and leave you poorly readable code...Kreager
@Arioch'The: I suggest you update the answer with these clarification details or remove it as misleading.Carrefour
@KromStern That is exactly why i asked you to update your question. As of now, this answer is needed, just as the mark "DEAD END" for now one to waste his time on this idea. You can see i ncomments that many very experienced people jumped on the wagon and assured me "oh, yes, it works" after i made initial suggestion. So this idea is compelling and obvious and no one expected the failure. That is why it should be recorded for everyone to save his time and not bringing this failed idea again. But i agree that failed answer is not an answer, so - again - that is why i asked to to update the Q.Kreager
Put the short but detailed enough extract of this answer in your very question, tell why it is a dead end, and then this answer can be deleted with no loss for anyone. PS. Did you tried using System.Length(array) after introducing those overloads ? This might still be an ugly but potentially usable solution afterall. You would have to use unqualified Length for strings (ur one in D7 and stock one in XE5) and ensure stock one used for arrays using explicit unit-qualificationKreager
@Arioch'The: I would not like to add that to the question because it is shaped to be broad enough to accept different approaches. I have made my answer into community wiki.Carrefour
So, does System.Length(array) work in D7 if you implement custom Length(string); overload; ? Test please. That makes a difference between non-working and non-convenient. Denoting tried and not-working approach has nothing against being broad in accepting working approaching.Kreager
T
1

Since Delphi 10.4 Sydney default value of ZEROBASEDSTRINGS is OFF for all platforms!

Twayblade answered 11/6, 2020 at 4:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.