Style: arrays vs containers

Insofar as it can be generalized, what is everyone’s experience with using built-in arrays/strings vs their library counterparts? Assume we are talking about applications with no hard resource constraints.

My reflection is that Ada arrays are so potent that it is meaningful to consider using them when designing an application.
This is remarkable because in C++, my working language, the impulse is to just go straight for std::string and std::vector. This is a necessity given the fragility of C “arrays” (pointers), but it also simplifies the choice.

Recently, I implemented an encoding library in Ada without any standard library facilities for strings or containers. It works solely with arrays.

For example, I can return two variable-length arrays from a function using a variant record:

type Decode_Result (Info_Length : Positive; Payload_Length : Natural) is
record
   Valid   : Boolean;
   Info    : String (1 .. Info_Length);
   Payload : Data (1 .. Payload_Length);
end record;

One can just nod in satisfaction. That’s not something I could do without allocations and pointers in C++.
Yet, I wonder, is there a point at which this become brittle and a burden?
I think built-in arrays and library containers don’t mix too well, so one way should be followed start to finish as much as possible.

The following is a higher-level code:

type Decode_Result is
record
   Valid   : Boolean;
   Info    : Unbounded_String;
   Payload : Vector;
end record;

In principle, I prefer to stick to higher-level and more universal abstractions.
Based on that alone I should probably start using Ada.Containers.*Vectors and Ada.Strings.Unbounded_Strings exclusively for everything and ditch language-level constructs.

I’m interested to hear what others feel leads to more robust code.
Can arrays and library abstractions be mixed at will or should programmers stick to either of the camps?

1 Like

Not every problem tells you the payload length in advance, and I’ve been in many situations where it’s impossible to know in advance how many elements there will be. There are ways to handle that without resorting to Containers, but as far as I know they require one to manually handle the heap or else engage in an unpleasant recursion and risk a stack overflow.

I do not consider vector containers higher abstraction. They are an alternative, often inferior. implementation of the same abstraction: array.

I basically never use Unbounded_String because there are very few use cases for. Of course, something like payload cannot be a vector just because you must read/write it as whole. It is a vector design issue, there could implementations that offer an array view, but then, why not an array from the start? However, there are cases where you would need a sort of container, e.g. when you read or create chunks of something like chunked data transfer or a syntax tree. In my experience it often requires some customization and containers do not fly again.

In essence usefulness of container libraries is hugely overestimated. There certainly exist cases when one could use them but the first impulse must be, wait, can I do it in an Ada 83 way? Do I need to add or remove elements, really? etc.

You will encounter problems if you want to add a Decode_Result member in a new record type. You’ll have to use an access instead, or define a holder type that will hide the machinery of an access to the users.

You can always allocate more and re-allocate if you run out of space. Which is faster between arrays and container/lists depends on the task at hand. If speed doesn’t matter then arrays are nice to me.

…which containers do automatically, certainly more reliably, and probably more efficiently for the general case. At least if the compiler / library writers know what they’re doing.

This is a risky reduction.
“General case” means inefficient almost by definition.
I’d say a container should offer convenience and safety at cost of decreased efficiency.

More generally, Ada’s containers are notoriously inefficient because of tampering checks.
Each container received a “Stable” version in Ada 2022 that removes these checks in hope to regain efficiency.
You can view a very long discussion about this issue in AI12-0111.

Sure; that’s absolutely a valid point. I’m not necessarily referring to the containers in Ada’s standard library. Rather, I’m replying to what strikes me as what I perceive as a strange point of view of some Ada users in this and other posts that “I’ve never needed it, so no one else must need it, and they should do it the same way I’ve always done it.”

There’s a real advantage to a language’s users to having a standard library of containers they can count on without worrying about doing everything themselves, and it seems (to me, anyway) that pretty much everyone in that thread you link recognizes that

Otherwise, why would they have the conversation in the first place?

1 Like

I like “Container Vectors” since it facilitates simple code. For example “Delete_Last” and “Last_Element” are favourites. Below is an example to simplify multi-tasking for brute force elaboration of many alternatives/candidates (to find the best). If you do not like the layout - you may ask ChatGPT to beautify :slight_smile:

``

  protected candidates1 is
     procedure store1(mgm1 : mgm1_p.Vector);
     procedure get1(mgm1 : out mgm1_p.Vector);
  private
     mgms1 : mgms1_p.Vector;
  end candidates1;

  protected body candidates1 is
     procedure store1(mgm1 : mgm1_p.Vector) is
     begin
        mgms1.Append(mgm1);
     end store1;

     procedure get1(mgm1 : out mgm1_p.Vector) is
     begin
       if mgms1.Is_Empty then
          mgm1 := mgm1_p.Empty_Vector;
          return;
       end if;
       mgm1 := mgms1.Last_Element;
       mgms1.Delete_Last;
     end get1;
  end candidates1;

  protected keep_best1 is
     procedure store_best1(mgm1 : mgm1_p.Vector; fit1 : real);
     function  get_best1 return mgm1_p.Vector;
  private
     mgm1_best : mgm1_p.Vector;
     fit1_best : real := real'Last;
  end keep_best1;

  protected body keep_best1 is
     procedure store_best1(mgm1 : mgm1_p.Vector; fit1 : real) is
     begin
       if fit1 < fit1_best then
          mgm1_best := mgm1;
          fit1_best := fit1;
       end if;
     end store_best1;

     function get_best1 return mgm1_p.Vector is (mgm1_best);
  end keep_best1;

``

It is a good illustration of dangers components/containers impose.

You should not use containers inside protected actions. Even if the implementation does not deadlock, accessing the memory pool still takes too much time and may significantly increase influence program timings.

Bounded containers

The bounded containers are better in this case, since all the memory they use is preallocated, though you will of course need to know the maximum possible number of elements that will be stored.

Thanks for response, I will notice.

I checked my special example of use, and the time spent scales close to the inverse of number of processors. But maybe not do this in flight control :slight_smile: