Jossi
Jossi

Reputation: 1080

Storing string references

Problem

There are multiple ways to store string reference, so how would you do it in the example code? Currently the problem is with storing access to string because it is causing non-local pointer cannot point to local object. Is storing 'First and 'Last to reference a string a preferable way?

String reference storage

This record stores reference to a string. The First and Last is supposed to point to a string. The Name should be able to the same I think, but that will cause non-local pointer cannot point to local object when a local string is assigned to that. So the current work around solution is to use First and Last.

type Segment is record
   First : Positive;
   Last : Positive;
   Length : Natural := 0;
   Name : access String;
end record;

Assigning sub string reference

The commented line is causing non-local pointer cannot point to local object. This is because Item is local. Source is not local and that is the string I want sub string references from.

procedure Find (Source : aliased String; Separator : Character; Last : out Natural; Item_Array : out Segment_Array) is
   P : Positive := Source'First;
begin
   for I in Item_Array'Range loop
      declare
         Item : aliased String := Separated_String_Next (Source, Separator, P);
      begin
         exit when Item'Length = 0;
         Item_Array (I).Length := Item'Length;
         Item_Array (I).First := Item'First;
         Item_Array (I).Last := Item'Last;
         --Item_Array (I).Name := Item'Access;
         Last := I;
      end;
   end loop;
end;

Example

with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Main is

   use Ada.Text_IO;
   use Ada.Integer_Text_IO;

   function Separated_String_Next (Source : String; Separator : Character; P : in out Positive) return String is
      A : Positive := P;
      B : Positive;
   begin
      while A <= Source'Last and then Source(A) = Separator loop
         A := A + 1;
      end loop;
      P := A;
      while P <= Source'Last and then Source(P) /= Separator loop
         P := P + 1;
      end loop;
      B := P - 1;
      while P <= Source'Last and then Source(P) = Separator loop
         P := P + 1;
      end loop;
      return Source (A .. B);
   end;

   type Segment is record
      First : Positive;
      Last : Positive;
      Length : Natural := 0;
      Name : access String;
   end record;

   type Segment_Array is array (Integer range <>) of Segment;

   procedure Find (Source : String; Separator : Character; Last : out Natural; Item_Array : out Segment_Array) is
      P : Positive := Source'First;
   begin
      for I in Item_Array'Range loop
         declare
            Item : aliased String := Separated_String_Next (Source, Separator, P);
         begin
            exit when Item'Length = 0;
            Item_Array (I).Length := Item'Length;
            Item_Array (I).First := Item'First;
            Item_Array (I).Last := Item'Last;
            --Item_Array (I).Name := Item'Access;
            Last := I;
         end;
      end loop;
   end;

   Source : String := ",,Item1,,,Item2,,Item3,,,,,,";
   Item_Array : Segment_Array (1 .. 100);
   Last : Natural;

begin

   Find (Source, ',', Last, Item_Array);

   Put_Line (Source);
   Put_Line ("Index First Last Name");
   for I in Item_Array (Item_Array'First .. Last)'Range loop
      Put (I, 5);
      Put (Item_Array (I).First, 6);
      Put (Item_Array (I).Last, 5);
      Put (" ");
      Put (Source (Item_Array (I).First .. Item_Array (I).Last));
      New_Line;
   end loop;

end;

Output

,,Item1,,,Item2,,Item3,,,,,,
Index First Last Name
    1     3    7 Item1
    2    11   15 Item2
    3    18   22 Item3

Upvotes: 2

Views: 158

Answers (1)

user1818839
user1818839

Reputation:

The error message tells you exactly what is wrong : Item is a string declared locally, i.e. on the stack, and you are assigning its address to an access type (pointer). I hope I don't need to explain why that won't work.

The immediate answer - which isn't wrong but isn't best practice either, is to allocate space for a new string - in a storage pool or on the heap - which is done with new.

Item : access String := new String'(Separated_String_Next (Source, Separator, P));
...
Item_Array (I).Name := Item;

Note that some other record members, at least, Length all appear to be completely redundant since it is merely a copy of its eponymous attributes, so should probably be eliminated (unless there's a part of the picture I can't see).

There are better answers. Sometimes you need to use access types, and handle their object lifetimes and all the ways they can go wrong. But more often their appearance is a hint that something in the design can be improved : for example:

  • the Unbounded_String may manage your strings more simply
  • You could use the length as a discriminant on the Segment record, and store the actual string (not an Access) in the record itself
  • Ada.Containers are a standard library of containers to abstract over handling the storage yourself (much as the STL is used in C++).
  • If you DO decide you need access types, it's better to use a named access type type Str_Access is access String; - then you can create a storage pool specific to Str_Acc types, and release the entire pool in one operation, to simplify object lifetime management and eliminate memory leaks.

Note the above essentially "deep copies" the slices of the Source string. If there is a specific need to "shallow copy" it - i.e. refer to the specific substrings in place - AND you can guarantee its object lifetime, this answer is not what you want. If so, please clarify the intent of the question.


For a "shallow copy" the approach in the question essentially fails because Item is already a deep copy ... on the stack.

The closest approach I can see is to make the source string aliassed ... you MUST do as you want each Segment to refer to it ... and pass its access to the Find procedure.

Then each Segment becomes a tuple of First, Last, (redundant Length) and access to the entire string (rather than a substring).

   procedure Find (Source : access String; Separator : Character; 
                   Last : out Natural; Item_Array : out Segment_Array) is
      P : Positive := Source'First;
   begin
      for I in Item_Array'Range loop
         declare
            Item : String := Separated_String_Next (Source.all, Separator, P);
         begin
            exit when Item'Length = 0;
...
            Item_Array (I).Name := Source;
            Last := I;
         end;
      end loop;
   end;

   Source : aliased String := ",,Item1,,,Item2,,Item3,,,,,,";
...
   Find (Source'access, ',', Last, Item_Array);

   for I in Item_Array (Item_Array'First .. Last)'Range loop
...
      Put (Item_Array (I).Name(Item_Array (I).First .. Item_Array (I).Last));
      New_Line;
   end loop;

A helper to extract a string from a Segment would probably be useful:

function get(S : Segment) return String is
begin
   return S.Name(S.First .. S.Last);
end get;
...
Put (get(Item_Array (I));

The only rationale I can see for such a design is where the set of strings to be parsed or dissected will barely fit in memory so duplication must be avoided. Perhaps also embedded programming or some such discipline where dynamic (heap) allocation is discouraged or even illegal.

I see no solution involving address arithmetic within a string, since an array is not merely its contents - if you point within it, you lose the attributes. You can make the same criticism of the equivalent C design : you can identify the start of a substring with a pointer, but you can't just stick a null terminator at the end of the substring without breaking the original string.

Given the bigger picture ... what you need, rather than the low level details of how you want to achieve it, there are probably better solutions.

Upvotes: 2

Related Questions