Is there a way to have a reference to discriminant-dependent components?

I needed to make a configurable container with the strong requirement to use as little RAM as possible while maintaining a good level of Item indexing.
I was happy for a long time with the implementation when the Items were simple.
But starting to use bigger structures as Items, I ruined the CPU performance because to set a simple field of structure, I need to read the whole structure temporaly for change one field.

I tried to do some dirty tricks to have a reference of the whole items structures to just change one field, but the compiler doesn’t like to give a reference. It says: “Illegal attribute for discriminant- dependent component”. It’s failing in the function “Reference”

Does anybody know a workaround for this? May I start from scratch with another configurable container without a discriminant?

Here is a simplified example:

generic

   type ItemType is private;
   type Kind_of_Item is (<>);
package Containers is

   type NodeType (Valid : Boolean := False) is record
      case Valid is
      when False => null;
      when True  => Content : ItemType;
      end case;
   end record;
   type DT_Actives is array (Kind_of_Item) of Boolean;

   type XC_Content is array (Kind_of_Item) of NodeType;
   type XC_Contentptr is access all XC_Content;

   type Container is tagged record
      cont : aliased XC_Content;
   end record;
   function Get (S: Container; K: Kind_of_Item) return ItemType is (S.cont(K).Content);
   
   procedure Set (S: in out Container; K: Kind_of_Item; Item : ItemType);
   
   --- Dirty tricks to try to ennhace speed:
   type Reference_Type (Element : not null access ItemType) is null record
        with Implicit_Dereference => Element;
      function Reference
     (S : aliased in out Container; I : Kind_of_Item) return Reference_Type;
      function Reference
     (S : aliased in out Container;
      I : Kind_of_Item) return Reference_Type is
        (Reference_Type'(Element => S.cont(I).Content'Access));
   
end Containers;
with Containers;

procedure Main is

   type MyStructure is record
      A : Integer := 1;
      B : Integer := 1;
   end record;
   type MyKindOfItem is (kind1, kind2, kind3);
   
   
   package mypackage is new Containers(MyStructure,MyKindOfItem);
   
   --Now we are happy to active only the needed kind to save RAM:
   Actives : mypackage.DT_Actives :=
     (Kind1 => True,
      Kind2 => False,
      Kind3 => True);
   
   mycontainer : mypackage.Container;
   
   temp : MyStructure;
begin
   -- I just want to change B field of kind3. Need to temporary read all
   temp := mycontainer.Get (kind3);
   temp.B := 5;
   mycontainer.Set (kind3,temp);
end Main;

My first note is about the design. Container is an implementation. It has no meaning by itself. So Get/Set have no meaning too. Your type must rather have operations reflecting the problem space. I would declare an opaque type with an array as a private implementation.

Typically for implementation I use an unbounded array of access types. The corresponding generic package is this:

The package maintains deallocation. If you store an item created by new the container manages the reference once put into the container. Get returns a pointer, so there is no copying involved. Discriminants is no problem.

A more elaborated schema, that supports limited types as well, involves smart pointers and containers of smart pointers:

Operations are defined on the specialized pointers (handles).

1 Like

If you are using GNAT, ‘Unrestricted_Access rather than ‘Access will allow you to do almost anything, no matter how unsafe … ;-}

1 Like

Thank you, Dmitry. I will deeply study your links.
Let me ask you, just to double check if it can work for me in the meanwhile I study them. I have these previous requirements:

  • No dynamic memory is allowed. Every object needs to be known at compilation time.
  • I cannot use the “new” operator, or at least I only can use it in a time-constrained boot sequence. (Better in a storage pool arena like)
  • The Index_Type will need to be the same and shared across a lot of software packages. But for another different “configurations”, some Index will not be used and any presumible space for them never used to save RAM.
    For example:
    – Every software will share and use this:
    type Index is (Cars, Motorbikes, Boats);
    But for example, there is a software for making wheels that will never use “Boats” elements, (although we need to use the same Index_type) and no RAM space will be reserved for them.

In any case, thank you for give me a link to your container solution!. I will study anyway

thank you!, It compiles with Unrestricted_Access!.. but now I’m a little afraid to go ahead with this.
I don’t have yet the knowledge to know if it’s an unsafe solution.

Without dynamic allocation it would be a plain array.

How does what you’re doing differ from

generic -- Containers
   type Itemtype is private;
   type Kind_of_Item is (<>);
package Containers is
   type Nodetype (Valid : Boolean := False) is record
      case Valid is
      when False => null;
      when True  => Content : Itemtype;
      end case;
   end record;

   type Container is array (Kind_of_Item) of Nodetype;
end Containers;

with Containers;
procedure Example is
   type Mystructure is record
      A : Integer := 1;
      B : Integer := 1;
   end record;

   type Kind_ID is (Kind1, Kind2, Kind3);   
   
   package Structure_Containers is new
      Containers (Itemtype => Mystructure, Kind_Of_Item => Kind_ID);

   Container : Structure_Containers.Container :=
      (Kind1 | Kind3 => (Valid => True, others => <>), Kind2 => <>);
begin -- Example
   Container (Kind3).B := 5;
end Example;

except for added complexity? DT_Actives has no effect, and your array will typically use the same amount of storage regardless of the values of its components.

1 Like

Yes, sorry, in my simplified example, I forgot to write a constructor function that receives the boolean array:

mycontainer : Container := F_Construye(Actives);

where the constructor was:

   function F_Construye (Actives : DT_Actives) return Container is
     begin
      return result : Container do        
         for I in DT_Actives'Range loop
            if Actives (I) then
               result.cont (I) := NodeType'(Valid => True, Content => F_Init);
            else               
               result.cont (I) := NodeType'(Valid => False);
            end if;
         end loop;
      end return;

The “F_Init” function is passed to the generics to Initialise every ItemType

Your example can indeed build the container directly.

Ok, so I had to do this for a project when there were still some bugs in the GNAT implementation of Ada 2012… As I recall, my solution was something like this:

Task Type Whatever(Index : Positive) is --…
Type Handle is not null access Whatever;
Type Tasks  is array(Positive range <>) of Handle;

Function Make_Tasks( Number : Natural ) return Tasks is
  Count : Positive := 1;
  Function Make return Handle is
  Begin
    Return Result : Constant Handle := new Whatever'(Index => Count) do
      Count:= Positeve'Succ(Count);
    End return;
  End Make;
Begin
  Return Result : Tasks:= (1..Number => Make);
End Make_Tasks;

Hope that helps.

But if you replace others => <> with Content => Init in my example, it is still equivalent to yours, simpler, and probably faster. When Kind_ID is large you have to write a large aggregate either way, so having the container constructor doesn’t buy you much, if anything.

Your original post emphasizes a need to minimize storage, be “fast”, and avoid heap usage. I can’t think of any way to do all those together. Directly accessing the array will be fast, but will either result in unused storage (most compilers, including GNAT) or involve the heap (Janus/Ada implements variant records this way). Minimizing storage and avoiding heap would probably involve a custom bounded map, but that would be slower.