❌ About FreshRSS

Normal view

There are new articles available, click to refresh the page.
Before yesterdaySoftware Orthodoxy

Parallel Comparison of C++ and Ada Producer-Consumer Implementations

 

Producer Consumer Comparison 

The C++ source code for this example is taken from the blog post here by Andrew Wei. A detailed description of the C++ software is given in the blog post.

This solution is shown in parallel with a corresponding Ada solution.

Both C++ and Ada separate interface specifications from implementation. C++ uses the header file, in this case the file named Buffer.hpp, to provide the interface specification for the buffer used in this example. C++ is not very strict about what goes into a header file and what goes into a .cpp file. The Ada version creates an Ada package. The Ada package specification defines the task types named producer_Int and consumer_Int. The buffer shared by all instances of producer_int and consumer_int is defined within the Ada package body file.

Interface Specification Files

Ada C++
package pc_tasks is
   task type produce_Int (Id : Natural);
   task type consume_Int (Id : Natural);
end pc_tasks;
//
//  Buffer.hpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/31/21.
//

#ifndef Buffer_hpp
#define Buffer_hpp

#include <mutex>
#include <condition_variable>
#include <stdio.h>
 
#define BUFFER_CAPACITY 10

class Buffer {
    // Buffer fields
    int buffer [BUFFER_CAPACITY];
    int buffer_size;
    int left; // index where variables are put inside of buffer (produced)
    int right; // index where variables are removed from buffer (consumed)
    
    // Fields for concurrency
    std::mutex mtx;
    std::condition_variable not_empty;
    std::condition_variable not_full;
    
public:
    // Place integer inside of buffer
    void produce(int thread_id, int num);
    
    // Remove integer from buffer
    int consume(int thread_id);
    
    Buffer();
};

#endif /* Buffer_hpp */

This comparison shows the interface for Ada task types while the C++ interface (.hpp) file shows the interface for the Buffer class. The C++ interface definition defines the interfaces, both public and private, to the Buffer class.

Shared Buffer Implementations

The Ada package body contains both the definition and implementation of the shared buffer object and the implementation of the task types.

Ada C++
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Numerics.Discrete_Random;

package body pc_tasks is
   type Index_T is mod 10; -- Modular type for 10 values
   type Circular_Array is array (Index_T) of Integer;

   protected buffer is
      entry produce (Item : in Integer);
      entry consume (Item : out Integer);
   private
      Buf      : Circular_Array;
      P_Index  : Index_T := Index_T'First;
      C_Index  : Index_T := Index_T'First;
      Buf_Size : Natural := 0;
   end buffer;

   protected body buffer is
      entry produce (Item : in Integer) when Buf_Size < Index_T'Modulus is
      begin
         Buf (P_Index) := Item;
         P_Index       := P_Index + 1;
         Buf_Size      := Buf_Size + 1;
      end produce;

      entry consume (Item : out Integer) when Buf_Size > 0 is
      begin
         Item     := Buf (C_Index);
         C_Index  := C_Index + 1;
         Buf_Size := Buf_Size - 1;
      end consume;
   end buffer;

   task body produce_Int is
      subtype decimal is Integer range 1 .. 10;
      package rand_int is new Ada.Numerics.Discrete_Random (decimal);
      use rand_int;
      value : decimal;
      seed  : Generator;
   begin
      Reset (seed);
      for I in 1 .. 4 loop
         value := Random (seed);
         buffer.produce (value);
         Put_Line ("Task" & Id'Image & " produced" & value'Image);
         delay 0.1;
      end loop;
   end produce_Int;

   task body consume_Int is
      Num : Integer;
   begin
      for I in 1 .. 6 loop
         buffer.consume (Num);
         Put_Line ("Task" & Id'Image & " consumed" & Num'Image);
         delay 0.1;
      end loop;
   end consume_Int;

end pc_tasks;
//
//  Buffer.cpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/31/21.
//

#include <iostream>
#include "Buffer.hpp"

Buffer::Buffer() {
    buffer_size = 0;
    left = 0;
    right = 0;
}

void Buffer::produce(int thread_id, int num) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    std::cout << "thread " << thread_id << " produced " << num << "\n";
    
    // Wait if the buffer is full
    not_full.wait(unique_lock, [this]() {
        return buffer_size != BUFFER_CAPACITY;
    });
    
    // Add input to buffer
    buffer[right] = num;
    
    // Update appropriate fields
    right = (right + 1) % BUFFER_CAPACITY;
    buffer_size++;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that buffer isn't empty
    not_empty.notify_one();
}

int Buffer::consume(int thread_id) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    // Wait if buffer is empty
    not_empty.wait(unique_lock, [this]() {
        return buffer_size != 0;
    });
    
    // Getvalue from position to remove in buffer
    int result = buffer[left];
    
    std::cout << "thread " << thread_id << " consumed " << result << "\n";
    
    // Update appropriate fields
    left = (left + 1) % BUFFER_CAPACITY;
    buffer_size--;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that the buffer isn't full
    not_full.notify_one();
    
    // Return result
    return result;
}

The Ada part of the package implementing the shared buffer is isolated below, along with a repeat of the C++ Buffer class implementation.

Ada C++
   type Index_T is mod 10; -- Modular type for 10 values
   type Circular_Array is array (Index_T) of Integer;

   protected buffer is
      entry produce (Item : in Integer);
      entry consume (Item : out Integer);
   private
      Buf      : Circular_Array;
      P_Index  : Index_T := Index_T'First;
      C_Index  : Index_T := Index_T'First;
      Buf_Size : Natural := 0;
   end buffer;

   protected body buffer is
      entry produce (Item : in Integer) when Buf_Size < Index_T'Modulus is
      begin
         Buf (P_Index) := Item;
         P_Index       := P_Index + 1;
         Buf_Size      := Buf_Size + 1;
      end produce;

      entry consume (Item : out Integer) when Buf_Size > 0 is
      begin
         Item     := Buf (C_Index);
         C_Index  := C_Index + 1;
         Buf_Size := Buf_Size - 1;
      end consume;
   end buffer;
//
//  Buffer.cpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/31/21.
//

#include <iostream>
#include "Buffer.hpp"

Buffer::Buffer() {
    buffer_size = 0;
    left = 0;
    right = 0;
}

void Buffer::produce(int thread_id, int num) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    std::cout << "thread " << thread_id << " produced " << num << "\n";
    
    // Wait if the buffer is full
    not_full.wait(unique_lock, [this]() {
        return buffer_size != BUFFER_CAPACITY;
    });
    
    // Add input to buffer
    buffer[right] = num;
    
    // Update appropriate fields
    right = (right + 1) % BUFFER_CAPACITY;
    buffer_size++;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that buffer isn't empty
    not_empty.notify_one();
}

int Buffer::consume(int thread_id) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    // Wait if buffer is empty
    not_empty.wait(unique_lock, [this]() {
        return buffer_size != 0;
    });
    
    // Getvalue from position to remove in buffer
    int result = buffer[left];
    
    std::cout << "thread " << thread_id << " consumed " << result << "\n";
    
    // Update appropriate fields
    left = (left + 1) % BUFFER_CAPACITY;
    buffer_size--;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that the buffer isn't full
    not_full.notify_one();
    
    // Return result
    return result;
}

 The Ada buffer definition begins by defining the array type to be used in the shared buffer. Ada allows arrays to be indexed by any discrete type. In this example an Ada modular type is declared and named Index_T. Type Index_T is declared to be "mod 10", which specifies that all its values are in the range of 0 through 9 and all the arithmetic operators return a value within this range. The only arithmetic operator used in this example is "+". Addition on a modular type is modular. Thus, in this example, 9 + 1 yields 0, which is exactly as needed for a circular buffer.

Type Circular_Array is an array indexed by Index_T. Every element of Circular_Array is an Integer.

Ada protected objects are protected against race conditions. They are specifically used for data shared by Ada tasks. Ada tasks are commonly implemented as operating system threads, but may also be used on a bare-bones system using only a compiler-generated Ada runtime.

The protected object is named buffer. It is separated into a specification and an implementation, but both parts in this example are contained in the package body. The protected specification declares the name of the protected object. There are three kinds of methods that may be used inside a protected object: procedures, entries and functions. Procedures have exclusive unconditional read-write access to the data in the protected object. Entries have conditional read-write access to the data in a shared object. Functions have shared read-only access to the data in the protected object. This example only uses two entries. The private portion of the protected object contains the definition of the data members in the protected object. Buf is an instance of Circular_Array. P_Index is an instance of the Index_T type and is used by the producer to add new data to the protected object. C_Index is an instance of Index_T type and is used by the consumer to index data read from the protected object. Buf_Size is an instance of the Natural subtype of Integer. Natural is a pre-defined subtype of Integer with a minimum value of 0. Buf_Size is initialized with the value 0.

The protected body implements the two entries. Each entry has an associated condition which must evaluate to True for the entry to execute. All tasks calling an entry while its controlling condition evaluates to False are implicitly placed in an entry queue for the called entry using a queuing policy of First-In-First-Out (FIFO). The next entry call in the queue is serviced as soon as the condition evaluates to TRUE. Tasks suspended in the entry queue are given access to the protected entry before any new tasks, thus maintaining the temporal condition of the calls.

The produce entry can only write to the protected object when Buf_Size is less than Index_T'Modulus, which in this case evaluates to 10. The consumer entry can only read from the protected object when Buf_Size is greater than 0.

Each entry implicitly handles all locking and unlocking of the protected object. The protected object is locked just before the "begin" reserved word in each entry and is unlocked just before the "end" reserved word in each entry. The modular nature of the index manipulations as well as the implicit lock manipulations explain the relatively simple Ada code compared with the more verbose corresponding C++ code.

Thread Implementations

The Ada task implementations are also contained in the package body while the C++ thread implementations are contained in the main.cpp file. Those corresponding source code sections are compared below.

Ada C++
   task body produce_Int is
      subtype decimal is Integer range 1 .. 10;
      package rand_int is new Ada.Numerics.Discrete_Random (decimal);
      use rand_int;
      value : decimal;
      seed  : Generator;
   begin
      Reset (seed);
      for I in 1 .. 4 loop
         value := Random (seed);
         buffer.produce (value);
         Put_Line ("Task" & Id'Image & " produced" & value'Image);
         delay 0.1;
      end loop;
   end produce_Int;

   task body consume_Int is
      Num : Integer;
   begin
      for I in 1 .. 6 loop
         buffer.consume (Num);
         Put_Line ("Task" & Id'Image & " consumed" & Num'Image);
         delay 0.1;
      end loop;
   end consume_Int;
// Takes in reference to a buffer and adds a random integer
void produceInt(Buffer &buffer) {
    for (int i = 0; i < 4; i++) {
        // Generate random number between 1 and 10
        int new_int = rand() % 10 + 1;
        buffer.produce(i, new_int);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// Takes in reference to a buffer and returns the latest int added
// in the buffer
void consumeInt(Buffer &buffer) {
    for (int i = 0; i < 6; i++) {
        buffer.consume(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

The Ada tasks have visibility to the buffer protected object because the task bodies and the protected object are both defined within the same package body scope.

The produce_Int task defines an Integer subtype named decimal with valid values in the range of 1 through 10. That type is passed to the generic package Ada.Numerics.Discrete_Random so that random numbers in the range of 1 through 10 will be generated for this program. The random number seed is reset based upon the system clock using the Reset(seed) command. The for loop iterates through the values 1 through 4. Each iteration generates a new random value, writes the value to buffer.produce, outputs the value to standard output and then delays 0.1 seconds (100 milliseconds).

The consumer_int task defines a local integer variable named Num. The for loop iterates through the numbers 1 through 6. Each iteration calls buffer.consume, assigning the entry out parameter to Num, outputs the consumed value and delays 0.1 seconds.

Program Entry Points

While the program entry point for a C++ program is named "main", the program entry point for an Ada program may have any name chosen by the programmer. Note that the Ada entry point is a procedure, meaning it does not return any value and that procedure has no parameters.

Ada C++
with pc_tasks;    use pc_tasks;
with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
begin
   Put_Line("Executing code in main...");
   declare
      Produce_Task0 : produce_Int (0);
      Consume_Task0 : consume_Int (1);
      Produce_Task1 : produce_Int (2);
      Consume_Task1 : consume_Int (3);
      Produce_Task2 : produce_Int (4);
   begin
      null;
   end;
   Put_Line ("Done!");
end Main;
int main(int argc, const char * argv[]) {
    std::cout << "Executing code in main...\n";
    
    // Initialize random seed
    srand (time(NULL));
    
    // Create Buffer
    Buffer buffer;
    
    // Create a thread to produce
    std::thread produceThread0(produceInt, std::ref(buffer));
    
    std::thread consumeThread0(consumeInt, std::ref(buffer));
    
    std::thread produceThread1(produceInt, std::ref(buffer));
    
    std::thread consumeThread1(consumeInt, std::ref(buffer));
    
    std::thread produceThread2(produceInt, std::ref(buffer));
    
    produceThread0.join();
    produceThread1.join();
    produceThread2.join();
    consumeThread0.join();
    consumeThread1.join();
    
    std::cout << "Done!\n";
    return 0;
}

Ada does not provide a "join()" method. Instead, the code block in which a task or set of task is declared cannot complete until all the tasks within that code block complete. The idiom shown above declared the 5 task type instances within an inner code block, which does not complete until all five of the tasks have terminated. Upon completion of that inner block the message "Done!" is output to standard output.

Complete code listings for both languages

Ada C++
package pc_tasks is
   task type produce_Int (Id : Natural);
   task type consume_Int (Id : Natural);
end pc_tasks;
//
//  Buffer.hpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/31/21.
//

#ifndef Buffer_hpp
#define Buffer_hpp

#include <mutex>
#include <condition_variable>
#include <stdio.h>
 
#define BUFFER_CAPACITY 10

class Buffer {
    // Buffer fields
    int buffer [BUFFER_CAPACITY];
    int buffer_size;
    int left; // index where variables are put inside of buffer (produced)
    int right; // index where variables are removed from buffer (consumed)
    
    // Fields for concurrency
    std::mutex mtx;
    std::condition_variable not_empty;
    std::condition_variable not_full;
    
public:
    // Place integer inside of buffer
    void produce(int thread_id, int num);
    
    // Remove integer from buffer
    int consume(int thread_id);
    
    Buffer();
};

#endif /* Buffer_hpp */
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Numerics.Discrete_Random;

package body pc_tasks is
   type Index_T is mod 10; -- Modular type for 10 values
   type Circular_Array is array (Index_T) of Integer;

   protected buffer is
      entry produce (Item : in Integer);
      entry consume (Item : out Integer);
   private
      Buf      : Circular_Array;
      P_Index  : Index_T := Index_T'First;
      C_Index  : Index_T := Index_T'First;
      Buf_Size : Natural := 0;
   end buffer;

   protected body buffer is
      entry produce (Item : in Integer) when Buf_Size < Index_T'Modulus is
      begin
         Buf (P_Index) := Item;
         P_Index       := P_Index + 1;
         Buf_Size      := Buf_Size + 1;
      end produce;

      entry consume (Item : out Integer) when Buf_Size > 0 is
      begin
         Item     := Buf (C_Index);
         C_Index  := C_Index + 1;
         Buf_Size := Buf_Size - 1;
      end consume;
   end buffer;

   task body produce_Int is
      subtype decimal is Integer range 1 .. 10;
      package rand_int is new Ada.Numerics.Discrete_Random (decimal);
      use rand_int;
      value : decimal;
      seed  : Generator;
   begin
      Reset (seed);
      for I in 1 .. 4 loop
         value := Random (seed);
         buffer.produce (value);
         Put_Line ("Task" & Id'Image & " produced" & value'Image);
         delay 0.1;
      end loop;
   end produce_Int;

   task body consume_Int is
      Num : Integer;
   begin
      for I in 1 .. 6 loop
         buffer.consume (Num);
         Put_Line ("Task" & Id'Image & " consumed" & Num'Image);
         delay 0.1;
      end loop;
   end consume_Int;

end pc_tasks;
//
//  Buffer.cpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/31/21.
//

#include <iostream>
#include "Buffer.hpp"

Buffer::Buffer() {
    buffer_size = 0;
    left = 0;
    right = 0;
}

void Buffer::produce(int thread_id, int num) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    std::cout << "thread " << thread_id << " produced " << num << "\n";
    
    // Wait if the buffer is full
    not_full.wait(unique_lock, [this]() {
        return buffer_size != BUFFER_CAPACITY;
    });
    
    // Add input to buffer
    buffer[right] = num;
    
    // Update appropriate fields
    right = (right + 1) % BUFFER_CAPACITY;
    buffer_size++;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that buffer isn't empty
    not_empty.notify_one();
}

int Buffer::consume(int thread_id) {
    // Acquire a unique lock on the mutex
    std::unique_lock<std::mutex> unique_lock(mtx);
    
    // Wait if buffer is empty
    not_empty.wait(unique_lock, [this]() {
        return buffer_size != 0;
    });
    
    // Getvalue from position to remove in buffer
    int result = buffer[left];
    
    std::cout << "thread " << thread_id << " consumed " << result << "\n";
    
    // Update appropriate fields
    left = (left + 1) % BUFFER_CAPACITY;
    buffer_size--;
    
    // Unlock unique lock
    unique_lock.unlock();
    
    // Notify a single thread that the buffer isn't full
    not_full.notify_one();
    
    // Return result
    return result;
}
with pc_tasks;    use pc_tasks;
with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
begin
   Put_Line("Executing code in main...");
   declare
      Produce_Task0 : produce_Int (0);
      Consume_Task0 : consume_Int (1);
      Produce_Task1 : produce_Int (2);
      Consume_Task1 : consume_Int (3);
      Produce_Task2 : produce_Int (4);
   begin
      null;
   end;
   Put_Line ("Done!");
end Main;
//
//  main.cpp
//  ProducerConsumer
//
//  Created by Andrew Wei on 5/30/21.
//

#include <thread>
#include <iostream>
#include "Buffer.hpp"
#include <stdlib.h>

// Takes in reference to a buffer and adds a random integer
void produceInt(Buffer &buffer) {
    for (int i = 0; i < 4; i++) {
        // Generate random number between 1 and 10
        int new_int = rand() % 10 + 1;
        buffer.produce(i, new_int);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// Takes in reference to a buffer and returns the latest int added
// in the buffer
void consumeInt(Buffer &buffer) {
    for (int i = 0; i < 6; i++) {
        buffer.consume(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main(int argc, const char * argv[]) {
    std::cout << "Executing code in main...\n";
    
    // Initialize random seed
    srand (time(NULL));
    
    // Create Buffer
    Buffer buffer;
    
    // Create a thread to produce
    std::thread produceThread0(produceInt, std::ref(buffer));
    
    std::thread consumeThread0(consumeInt, std::ref(buffer));
    
    std::thread produceThread1(produceInt, std::ref(buffer));
    
    std::thread consumeThread1(consumeInt, std::ref(buffer));
    
    std::thread produceThread2(produceInt, std::ref(buffer));
    
    produceThread0.join();
    produceThread1.join();
    produceThread2.join();
    consumeThread0.join();
    consumeThread1.join();
    
    std::cout << "Done!\n";
    return 0;
}

 Summary

The same producer-consumer problem can be solved using both Ada and C++. The C++ approach to the producer-consumer pattern requires far more attention to low level details than does the Ada approach to the producer-consumer pattern. The greatest difference in the two approaches is the requirement for the C++ programmer to explicitly manipulate mutex locking/unlocking and suspended thread wait and notification commanding.

  • 15 October 2023 at 22:53

Alternative to exceptions when attempting to pop an empty stack

 

A recent question on StackOverflow asked about whether or not a mutex is locked twice by the same thread without unlocking the mutex. See https://stackoverflow.com/questions/76890110/c-the-thread-repeatedly-locks-the-same-mutex-multiple-times?noredirect=1#comment135552146_76890110

The C++ source code presented in the question is:

1 #include <exception>
2 #include <memory>
3 #include <mutex>
4 #include <stack>
5
6 struct empty_stack : std::exception
7 {
8    const char* what() const throw();
9 };
10
11 template<typename T>
12 class threadsafe_stack
13 {
14 private:
15    std::stack<T> data;
16    mutable std::mutex m;
17 public:
18    threadsafe_stack(){}
19    threadsafe_stack(const threadsafe_stack& other) {
20       std::lock_guard<std::mutex> lock(other.m);
21       data = other.data;
22 }
23    threadsafe_stack& operator=(const threadsafe_stack& other) = delete;
24    void push(T new_value)
25    {
26       std::lock_guard<std::mutex> lk(m);
27       data.push(std::move(new_value));
28    }
29    std::shared_ptr<T> pop()
30    {
31       std::lock_guard<std::mutex> lock(m);
32       if (data.empty()) throw empty_stack();
33       std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
34       data.pop();
35       return res;
36    }
37    void pop(T& value)
38    {
39       std::lock_guard<std::mutex> lock(m);
40       if (data.empty()) throw empty_stack();
41       *value = data.top();
42       data.pop();
43    }
44    bool empty() const
45    {
46       std::lock_guard<std::mutex> lock(m);
47       return data.empty();
48    }
49 };
 

One issue with this program design, which has nothing to do with the question asked, is throwing an exception when pop is called on an empty stack. An empty stack is not an exceptional situation. In fact the stack starts off as an empty stack when it is created.

Exception handling should not be used to deal with non-exceptional program state.

The following Ada stack implementation demonstrates how an empty stack should be handled in a thread-safe manner.

The Ada program defines a generic thread package with the same operations shown in the C++ example above.

The Ada program creates a generic stack package and a main procedure. Ada packages are separated into two parts, the package specification and the package body or implementation. The package specification defines the package API.

1 with Ada.Containers.Doubly_Linked_Lists;
2 generic
3    type element_type is private;
4 package generic_stack is
5    package List_Pack is new
6       Ada.Containers.Doubly_Linked_Lists (element_type);
7 use List_Pack;
8
9 protected type Stack is
10    procedure Push (Item : element_type);
11    entry Pop (Item : out element_type);
12    function Is_Empty return Boolean;
13 private
14    Buffer : List := Empty_List;
15 end Stack;
16
17 end generic_stack;
 

The package creates an Ada protected type. A protected type or protected object is a passive unit of concurrency which is implicitly protected from race conditions. There are three kinds of methods available to be defined within a protected type. Protected procedures implicitly acquire an unconditional exclusive read-write lock on the data in the protected type. Protected entries implicitly acquire a conditional exclusive read-write lock on the data in the protected type. A task suspends while the entry boundary condition evaluates to false. The suspended tasks are placed in an entry queue so that they can be activated when the boundary condition evaluates to true. The third kind of method available for a protected object is a function. Protected functions are limited to read-only access to the data in the protected type. Protected functions acquire a shared read lock on the data in the protected object.

The implementation of the package is in a separate file containing the package body.

1  package body generic_stack is
2
3     -----------
4     -- Stack --
5     -----------
6
7     protected body Stack is
8
9        ----------
10       -- Push --
11       ----------
12
13       procedure Push (Item : element_type) is
14       begin
15          Buffer.Append (Item);
16       end Push;
17
18       ---------
19       -- Pop --
20       ---------
21
22       entry Pop (Item : out element_type) when
23                  not Buffer.Is_Empty is
24       begin
25          Item := Buffer.Last_Element;
26          Buffer.Delete_Last;
27       end Pop;
28
29       --------------
30       -- Is_Empty --
31       --------------
32
33       function Is_Empty return Boolean is
34       begin
35          return Buffer.Is_Empty;
36       end Is_Empty;
37
38    end Stack;
39
40 end generic_stack;

 

The push procedure implicitly locks the Buffer when it begins executing and unlocks the Buffer as the procedure completes.

The pop entry only executes when Buffer.Is_Empty evaluates to false. When that condition is met any immediate call, or any task waiting in the entry queue is allowed to execute the entry. The default queuing policy of the entry queue is FIFO and all tasks suspended in the entry queue will be executed before any new call can be executed. The shared read-write lock is applied when execution of the entry starts and is release upon completion of the entry.

The Is_Empty function can only execute when no read-write lock is applied to the Buffer data element. Upon starting execution the function applies a shared read lock, allowing any number of tasks to read simultaneously and preventing any procedure or function to execute until the function completes and releases its shared read lock.

The main procedure, which is the program entry point for this example, follows.

1  with Ada.Text_IO; use Ada.Text_IO;
2  with generic_stack;
3
4  procedure Main is
5     package int_stack is new generic_stack (Integer);
6     use int_stack;
7
8     The_Stack : Stack;
9
10    task producer is
11       entry Start;
12    end producer;
13
14    task body producer is
15    begin
16       accept Start;
17       Put_Line ("Producer started.");
18       for I in 1 .. 20 loop
19          The_Stack.Push (I);
20          Put_Line ("Pushed" & I'Image);
21          delay 0.001;
22       end loop;
23    end producer;
24
25    task consumer is
26       entry Start;
27    end consumer;
28
29    task body consumer is
30       Num : Integer;
31    begin
32       accept Start;
33       Put_Line ("Consumer started.");
34       for I in 1 .. 20 loop
35          The_Stack.Pop (Num);
36          Put_Line ("Popped" & Num'Image);
37       end loop;
38    end consumer;
39
40 begin
41    consumer.Start;
42    delay 0.01;
43    producer.Start;
44 end Main;
 

Line 5 creates an instance of the generic_stack package passing the type Integer as the generic parameter. This results in a stack of integer values.

Line 8 declares an instance of the Stack type named The_Stack.

Line 10 declares the interface for the producer task. This task has one entry named Start.

Line 14 declares the implementation of the producer task.

Line 16 accepts the Start entry. The producer task will suspend until another task calls its Start entry. Task entries implement a Rendezvous logic allowing synchronous coordination of tasks. After accepting Start the producer executes a for loop 20 times, each time pushing the current value of the loop control variable onto the stack, displaying a message to standard output and then delaying (sleeping) for 0.001 seconds.

The consumer task declaration begins at line 25. The consumer also has a Start entry.

The consumer task accepts its start entry at line 32 and then pops 20 values off of the stack. The consumer task has no delay statement. The consumer tasks will then complete its iteration before the producer task pushes another value onto the stack. Each call to the pop entry will therefore encounter an empty stack until the producer pushes another value onto the stack.

Both the producer task and the consumer tasks begin execution immediately, but both tasks suspend until their Start entry is called.

Line 40 begins execution of the main procedure which is also the top-level task for the program. Line 41 calls consumer.Start. Line 42 causes the main procedure to delay for 0.01 seconds before line 43 calls producer.start.

This delay in starting the tasks ensures that the consumer must wait at least 0.01 seconds before the first data will be pushed onto the stack. In other words, the stack will be empty for at least 0.01 seconds.

The output of this program is:

 

Consumer started.
Producer started.
Pushed 1
Popped 1
Pushed 2
Popped 2
Pushed 3
Popped 3
Pushed 4
Popped 4
Pushed 5
Popped 5
Pushed 6
Popped 6
Pushed 7
Popped 7
Pushed 8
Popped 8
Pushed 9
Popped 9
Pushed 10
Popped 10
Pushed 11
Popped 11
Pushed 12
Popped 12
Pushed 13
Popped 13
Pushed 14
Popped 14
Pushed 15
Popped 15
Pushed 16
Popped 16
Pushed 17
Popped 17
Pushed 18
Popped 18
Pushed 19
Popped 19
Pushed 20
Popped 20
 

Clearly the program did not encounter any exceptions, yet the consumer could only pop values after they were pushed onto the stack.

  • 13 August 2023 at 05:16

Array Handling

 

Arrays in both C and Ada are a compound type with the elements arranged sequentially in memory. All the elements in an array are of a single type. For instance an array may contain integer elements or it may contain floating point elements, or it may contain strings of characters, or it may even contain other arrays.

One might therefore assume that arrays in C and Ada are fundamentally the same. This article explores that assumption and finds some clear differences between C arrays and Ada arrays.

Array Characteristic

C language

Ada Language

Array types

C only allows definition of the type of an array element. It does not allow declaration of an array type.

Every Ada array is a member of a type, even if it is an anonymous type.

Index range

Valid C array indices always start at 0. The C language provides no implicit checking for referencing invalid array indices.

Ada array indices may begin at any programmer-chosen scalar value and end at any programmer-chosen scalar value. Ada compilers validate static array index references and by default the Ada runtime checks array index values during program execution.

Array declaration

C arrays are declared by specifying the element type, the array name, and the number of elements in the array.

Ada arrays are declared by specifying the array name, the array index range and the array element type.

Array size

C array sizes can only be calculated using the sizeof operator within the scope in which the array is first declared. Passing an array to a function results in only the address of the first array element being passed to the function. The programmer must pass another parameter to the function indicating the size of the array. The compiler cannot ensure the size parameter is correct.

Ada array attributes are available wherever the array is visible. Ada array attributes include:

  •      ‘Size – The number of bits the array occupies in memory.
  •        ‘Length – The number of elements in the array.
  •         ‘First – The first valid index value for the array.
  •      ‘Last – The last valid index value for the array
  •      ‘Range – The range specified by ‘First .. ‘Last

 

Array slicing

C does not provide a facility for array slicing.

Ada provides the ability to define a slice of an array.

Array assignment

C does not allow the programmer to copy one array to another using a single assignment statement.

Ada allow arrays to be copied using a single assignment statement.

The impact of these differences is demonstrated by reviewing the Merge-Sort algorithm implemented in both languages.

The first example is a C implementation of the Merge-Sort algorithm.

1 /*     
2 * C Program to Perform Merge Sort using Recursion and Functions
3 */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7
8 // merge function
9 void Merge(int arr[], int left, int mid, int right)
10 {
11    int i, j, k;
12    int size1 = mid - left + 1;
13    int size2 = right - mid;
14
15    // created temporary array
16    int Left[size1], Right[size2];
17
18    // copying the data from arr to temporary array
19    for (i = 0; i < size1; i++)
20        Left[i] = arr[left + i];
21
22    for (j = 0; j < size2; j++)
23       Right[j] = arr[mid + 1 + j];
24
25    // merging of the array
26    i = 0; // intital index of first subarray
27    j = 0; // inital index of second subarray
28    k = left; // initial index of parent array
29    while (i < size1 && j < size2)
30    {
31        if (Left[i] <= Right[j])
32        {
33            arr[k] = Left[i];
34            i++;
35        }
36        else
37        {
38            arr[k] = Right[j];
39            j++;
40        }
41        k++;
42    }
43
44    // copying the elements from Left[], if any
45    while (i < size1)
46    {
47        arr[k] = Left[i];
48        i++;
49        k++;
50    }
51
52    // copying the elements from Right[], if any
53    while (j < size2)
54    {
55        arr[k] = Right[j];
56        j++;
57        k++;
58    }
59 }
60
61 //merge sort function
62 void Merge_Sort(int arr[], int left, int right)
63 {
64    if (left < right)
65    {
66
67        int mid = left + (right - left) / 2;
68
69        // recursive calling of merge_sort
70        Merge_Sort(arr, left, mid);
71        Merge_Sort(arr, mid + 1, right);
72
73        Merge(arr, left, mid, right);
74    }
75 }
76
77 // driver code
78 int main()
79 {
80     int size;
81     printf("Enter the size: ");
82     scanf("%d", &size);
83
84     int arr[size];
85     printf("Enter the elements of array: ");
86     for (int i = 0; i < size; i++)
87     {
88         scanf("%d", &arr[i]);
89     }
90
91     Merge_Sort(arr, 0, size - 1);
92
93     printf("The sorted array is: ");
94     for (int i = 0; i < size; i++)
95     {
96         printf("%d ", arr[i]);
97     }
98     printf("\n");
99     return 0;
100 } 

Both the Merge_Sort function and the Merge function take several parameters.

 

62 void Merge_Sort(int arr[], int left, int right)
9 void Merge(int arr[], int left, int mid, int right) 

The first parameter in each function specifies an array of int elements. The remaining parameters specify various int values. These extra parameters are needed in C because all array indices must begin at 0, the size of the array parameter is not passed with the array, and C does not provide array slicing.

The C programmer must provide all this information as additional function parameters.

The C language does not provide assignment of one array to another. The programmer must explicitly create a loop and assign each element one at a time:

18    // copying the data from arr to temporary array
19    for (i = 0; i < size1; i++)
20        Left[i] = arr[left + i];
21
22    for (j = 0; j < size2; j++)
23       Right[j] = arr[mid + 1 + j];
 

Each of these examples is a demonstration of the low-level terseness of C. Each of these examples shows how much more writing must be done in C to achieve what Ada does very simply.

The Ada version of the Merge-Sort algorithm is implemented in a generic Ada package so that the sort procedure declared within the package specification can be used to sort any mutable data type.

The Ada solution is done using three filles.

1 generic
2    type element_type is private;
3    type array_type is array (Integer range <>) of element_type;
4    with function "<" (left, right : element_type) return Boolean is <>;
5 package generic_merge_sort is
6    procedure sort (Item : in out array_type);
7 end generic_merge_sort;

The generic package specification requires three parameters when an instance of the package is declared. The first parameter is the name of the element type for the array which will be sorted. The second parameter is the name of the array type to be sorted. This parameter is restricted to an unconstrained array type indexed by Integer values. Each element of the array type must be the type passed to the first parameter. The third parameter is a possible overloading of the “<” function. This is an optional parameter. If the element_type already has a “<” function defined that function will be used by default.

The procedure sort is defined to take one parameter with mode “in out”, meaning the parameter value(s) will be used and possibly modified within the procedure. All modifications will be visible to the calling scope.

The package body contains the logic used to implement the merge-sort algorithm.

1 package body generic_merge_sort is
2    procedure merge (Item : in out array_type) is
3       mid   : Integer    := Item'First + (Item'Last - Item'First) / 2;
4       Left  : array_type := Item (Item'First .. mid);
5       Right : array_type := Item (mid + 1 .. Item'Last);
6       I     : Integer    := Left'First;
7       J     : Integer    := Right'First;
8       K     : Integer    := Item'First;
9    begin
10      while I <= Left'Last and then J <= Right'Last loop
11         if Left (I) < Right (J) then
12            Item (K) := Left (I);
13            I        := I + 1;
14         else
15            Item (K) := Right (J);
16            J        := J + 1;
17         end if;
18         K := K + 1;
19      end loop;
20      -- copying unused items from Left
21      while I <= Left'Last loop
22         Item (K) := Left (I);
23         I        := I + 1;
24         K        := K + 1;
25      end loop;
26      -- copying unused items from right
27      while J <= Right'Last loop
28         Item (K) := Right (J);
29         J        := J + 1;
30         K        := K + 1;
31      end loop;
32   end merge;
33
34   ----------
35   -- sort --
36   ----------
37
38   procedure sort (Item : in out array_type) is
39      mid : Integer;
40   begin
41      if Item'Length > 1 then
42         mid := Item'First + (Item'Last - Item'First)/2;
43         sort(Item(Item'First .. Mid));
44         sort(Item(Mid + 1 .. Item'Last));
45         merge(Item);
46      end if;
47   end sort;
48
49 end generic_merge_sort;

This example make extensive use of Ada array attributes. The value of mid in both the merge procedure and the sort procedure is calculated using the ‘First and ‘Last array attributes, simplifying the procedure signature for both sort and merge. Both procedures only need an instance or slice of an array passed to them.

In the merge procedure the Left and Right instances of array_type are created and initialized with slices of the array Item passed to the procedure. Ada’s ability to assign one array or slice to another array or slice of the same type eliminates the need to explicitly calculate the sizes needed for the Left and Right arrays. It also eliminates the need to explicitly copy the values iteratively.

4       Left  : array_type := Item (Item'First .. mid);
5       Right : array_type := Item (mid + 1 .. Item'Last);

For reference, here is the corresponding C code:

12    int size1 = mid - left + 1;
13    int size2 = right - mid;
14
15    // created temporary array
16    int Left[size1], Right[size2];
17
18    // copying the data from arr to temporary array
19    for (i = 0; i < size1; i++)
20        Left[i] = arr[left + i];
21
22    for (j = 0; j < size2; j++)
23       Right[j] = arr[mid + 1 + j];

A comparison of the C merge_sort function and the Ada sort procedure provides more insight to the differences between C arrays and Ada arrays.

C merge_sort:

62 void Merge_Sort(int arr[], int left, int right)
63 {
64    if (left < right)
65    {
66
67        int mid = left + (right - left) / 2;
68
69        // recursive calling of merge_sort
70        Merge_Sort(arr, left, mid);
71        Merge_Sort(arr, mid + 1, right);
72
73        Merge(arr, left, mid, right);
74    }
75 }

Ada sort:

38   procedure sort (Item : in out array_type) is
39      mid : Integer;
40   begin
41      if Item'Length > 1 then
42         mid := Item'First + (Item'Last - Item'First)/2;
43         sort(Item(Item'First .. Mid));
44         sort(Item(Mid + 1 .. Item'Last));
45         merge(Item);
46      end if;
47   end sort;

The condition in the “if” statement, while achieving the same result, is quite different. The C function relies upon the correctness of the left and right parameters passed as the second and third arguments to the function. The Ada procedure only relies upon the ‘Length attribute of the array. The Ada syntax is much less error-prone and a more direct statement to a human reader about the intention of the conditional statement. In order to get a complete understanding of the purposes of the C left and right parameters one must read the context of the main procedure, while the meaning of the Ada conditional is completely clear in the local context. Since the sort parameter type is an unconstrained array type every slice of the array is also an instance of the unconstrained array type.

The C Merge_Sort function recursively calls itself passing the beginning of the arr parameter each time but with different values for left and right.

The Ada sort function recursively calls itself passing slices of the parameter Item to each recursive call. In this manner each recursion of Sort only sees the slice passed to it and all the other elements of the array initially passed to the sort procedure from main are not available to the recursive call.

The C Merge procedure passes the full array, but also passes three parameters containing the left, mid and right index positions in the array. The Ada merge procedure only array slice passed to it by the sort procedure.

The main function in C is:

78 int main()
79 {
80     int size;
81     printf("Enter the size: ");
82     scanf("%d", &size);
83
84     int arr[size];
85     printf("Enter the elements of array: ");
86     for (int i = 0; i < size; i++)
87     {
88         scanf("%d", &arr[i]);
89     }
90
91     Merge_Sort(arr, 0, size - 1);
92
93     printf("The sorted array is: ");
94     for (int i = 0; i < size; i++)
95     {
96         printf("%d ", arr[i]);
97     }
98     printf("\n");
99     return 0;
100 }

The main procedure in Ada is:

1 with Ada.Text_IO;         use Ada.Text_IO;
2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
3 with generic_merge_sort;
4
5 procedure Main is
6    type int_arr is array (Integer range <>) of Integer;
7    package int_sort is new generic_merge_sort
8           (element_type => Integer, array_type => int_arr);
9    use int_sort;
10   Num_Elements : Positive;
11 begin
12   Put ("Enter the size of the array: ");
13   Get (Num_Elements);
14   declare
15      the_array : int_arr (1 .. Num_Elements);
16   begin
17      Put_Line ("Enter the array values:");
18      for value of the_array loop
19         Get (value);
20      end loop;
21      sort (the_array);
22      Put_Line ("The sorted array is:");
23      for value of the_array loop
24         Put (value'Image & " ");
25      end loop;
26      New_Line;
27   end;
28 end Main;

Lines 6 through 9 of the Ada program are needed to create an instance of the generic_merge_sort package.

6    type int_arr is array (Integer range <>) of Integer;
7    package int_sort is new generic_merge_sort
8           (element_type => Integer, array_type => int_arr);
9    use int_sort;

An inner block is declared so that an array of the size entered by the user can be created. The rest of the program is performed in this inner block.

14   declare
15      the_array : int_arr (1 .. Num_Elements);
16   begin
17      Put_Line ("Enter the array values:");
18      for value of the_array loop
19         Get (value);
20      end loop;
21      sort (the_array);
22      Put_Line ("The sorted array is:");
23      for value of the_array loop
24         Put (value'Image & " ");
25      end loop;
26      New_Line;
27   end;

The for loop used to read all the values into the array starting at line 18 is an Ada iterator loop. The loop parameter “value” becomes an alias for each array element starting at the first element and ending at the last element. Thus, each value entered by the user is placed directly into the array without explicit index notation.

The array is sorted.

21      sort (the_array);

Finally, the sorted array is output to standard output and the program ends.

Conclusion:

The C and Ada examples implement the same merge-sort algorithm to sort an array of integer values. C arrays are more primitive abstractions than are Ada arrays. The more primitive abstractions require more lines of C source code than does the Ada implementation. The C implementation also requires a more complex parameter list for both its Merge function and its Merge_Sort function. The Ada implementation concentrates on merely sorting an array without additional parameters.

The built-in characteristics of Ada arrays really do simplify the manipulation of arrays, even when those arrays are passed to an Ada function or procedure.

I learned long ago that complexity is the enemy of correctness. In these examples we see that lower complexity also provides fewer opportunities for programmer error. We also see that a programming language with a low level syntax is not necessarily simpler than a programming language with a higher level syntax. Terse is not always simpler. Terse does not always result in less typing for the programmer.

  • 5 August 2023 at 20:36

Threads of Confusion

Many programming languages support concurrent behavior through the creation of threads and the explicit manipulation of semaphores, locks or mutexes. The C language seems to have initiated this approach to handling concurrency.

C example

The following example of using a mutex with thread comes from C Program to Show Thread Interface and Memory Consistency Errors – GeeksforGeeks

1       // C program to use a mutex to avoid memory consistency

2       // errors

3       #include <pthread.h>

4       #include <stdio.h>

5       #include <stdlib.h>

6

7       // Global variable that will be shared among threads

8       int shared_counter = 0;

9

10       // Mutex to protect the shared counter

11       pthread_mutex_t shared_counter_mutex

12       = PTHREAD_MUTEX_INITIALIZER;

13

14       // Function that will be executed by each thread

15       void* thread_function(void* thread_id)

16       {

17               // Get the thread ID

18               long tid = (long)thread_id;

19

20               // Lock the mutex to protect the shared counter

21               pthread_mutex_lock(&shared_counter_mutex);

22

23               // Increment the shared counter

24               shared_counter++;

25

26               // Print the thread ID and the updated value of the

27               // shared counter

28               printf("Thread %ld: shared_counter = %d\n", tid,

29                             shared_counter);

30

31               // Unlock the mutex

32               pthread_mutex_unlock(&shared_counter_mutex);

33

34               // Return NULL to indicate successful execution of the

35               // thread

36               return NULL;

37   }

38

39       int main(int argc, char* argv[])

40       {

41               // Check if the number of arguments is correct

42               if (argc != 2) {

43                       printf("Usage: %s <number_of_threads>\n", argv[0]);

44                       exit(EXIT_FAILURE);

45       }

46

47               // Get the number of threads to create from the command

48               // line arguments

49               int num_threads = atoi(argv[1]);

50

51               // Create an array of pthread_t structures to store the

52               // thread IDs

53               pthread_t* threads = (pthread_t*)malloc(

54                       num_threads * sizeof(pthread_t));

55

56               // Create the specified number of threads

57               for (int i = 0; i < num_threads; i++) {

58                       int status = pthread_create(

59                               &threads[i], NULL, thread_function, (void*)i);

60                       if (status != 0) {

61                               printf("Error: pthread_create() returned error "

62                                             "code %d\n",

63                                             status);

64                               exit(EXIT_FAILURE);

65           }

66       }

67

68               // Wait for all threads to finish execution

69               for (int i = 0; i < num_threads; i++) {

70                       int status = pthread_join(threads[i], NULL);

71                       if (status != 0) {

72                               printf("Error: pthread_join() returned error "

73                                             "code %d\n",

74                                             status);

75                               exit(EXIT_FAILURE);

76           }

77       }

78

79               // Free the memory allocated for the thread IDs

80               free(threads);

81

82               // Print the final value of the shared counter

83               printf("Final value of shared_counter: %d\n",

84                             shared_counter);

85

86               // Return success

87               return 0;

88   }

The C threading model uses the pthread library to create threads. The behavior of each created thread is controlled by the function passed to the thread as part of the pthread_create function parameter list. First an array of pthread_t structures is created:

53               pthread_t* threads = (pthread_t*)malloc(

54                       num_threads * sizeof(pthread_t));

Next, the Id number of each thread is created as the function named thread_function is passed to each element of the thread as a parameter to the pthread_create function:

57               for (int i = 0; i < num_threads; i++) {

58                       int status = pthread_create(

59                               &threads[i], NULL, thread_function, (void*)i);

60                       if (status != 0) {

61                               printf("Error: pthread_create() returned error "

62                                             "code %d\n",

63                                             status);

64                               exit(EXIT_FAILURE);

65           }

66       }

The third parameter to the pthread_create command is the function passed to the newly created thread. The fourth argument is the parameter passed to the function passed to the thread. Not only is the thread function parameter passed as the fourth argument to the pthread_create function, it is also cast to a pointer to void. This bit of syntax can be confusing because the value being passed is not a pointer, but rather an int.

Let's now look inside the function named thread_function, but before that, let's look at the creation of the pthread_mutex used to control access to the shared counter.

10       // Mutex to protect the shared counter

11       pthread_mutex_t shared_counter_mutex

12       = PTHREAD_MUTEX_INITIALIZER;

Yes, the pthread_mutex_t instance used to control access to the shared counter is itself a shared instance of a type. You might also see that the shared_counter_mutex is only loosely connected to the shared_counter integer variable. That loose connection is a voluntary connection not enforced by any syntax in the C pthreads library.

The thread_function is defined as:

14       // Function that will be executed by each thread

15       void* thread_function(void* thread_id)

16       {

17               // Get the thread ID

18               long tid = (long)thread_id;

19

20               // Lock the mutex to protect the shared counter

21               pthread_mutex_lock(&shared_counter_mutex);

22

23               // Increment the shared counter

24               shared_counter++;

25

26               // Print the thread ID and the updated value of the

27               // shared counter

28               printf("Thread %ld: shared_counter = %d\n", tid,

29                             shared_counter);

30

31               // Unlock the mutex

32               pthread_mutex_unlock(&shared_counter_mutex);

33

34               // Return NULL to indicate successful execution of the

35               // thread

36               return NULL;

37   }

The thread function must explicitly lock the shared_counter mutex, then increment the shared_counter, then output the current value of the shared_counter and finally unlock the shared_counter_mutex.

A major source of confusion

The order of the locking modifying and unlocking the mutex is critical. The shared_counter knows nothing of these locks. The pthread_mutex_t has no syntactical connection to the shared_counter. On the other hand, failing to lock, perform operations and then unlock the mutex will result in semantic failures which prevent the mutex from properly locking the shared_counter and then unlocking the shared counter after the operations are completed. Even worse, there is no syntax in the C pthread library prohibiting a thread from simply accessing the shared_counter while completely ignoring the mutex lock and unlock behaviors.

Further program issues

The remainder of the program calls pthread_join, forcing the main thread to wait until all the pthreads have completed before continuing to execute its sequence of instructions.

68               // Wait for all threads to finish execution

69               for (int i = 0; i < num_threads; i++) {

70                       int status = pthread_join(threads[i], NULL);

71                       if (status != 0) {

72                               printf("Error: pthread_join() returned error "

73                                             "code %d\n",

74                                             status);

75                               exit(EXIT_FAILURE);

76           }

77       }

It is important to understand that the pthread_join function called for each thread can fail and return failure status.

Once the pthreads have completed the program frees all the pointers in the array of pointers to pthread_t that were created to create the array of threads. This is done because it is always correct to free dynamically allocated memory when that memory is no longer needed in the program.

While this action is not particularly confusing, it is yet another detail that should be explicitly implemented.

Ada Example

The Ada programming language has tasking, which is roughly equivalent to threads, built into the core language since the first Ada language standard in 1983. In the 1995 standard protected types were added to the core language, which allow asynchronous communication between tasks. There are no special Ada libraries to implement tasking or protected types.

A protected type or protected object is protected against inappropriate simultaneous access to a shared data structure. A protected type or object is built very much in the philosophy of Object Oriented Programming inasmuch as the protected object has programmer-defined behaviors, however the locking and unlocking of the protected object is performed implicitly by the object itself and not through explicit lock and unlock methods called by the task accessing the protected object.

There are three kinds of protected methods.

  • Protected procedures – Protected procedures control an unconditional exclusive read-write lock on the protected object. The task calling the protected procedure can access the protected object whenever the object is unlocked.

  • Protected entries – Protected entries control a conditional exclusive read-write lock on the protected object. The calling task can only execute the entry when its boundary condition evaluates to True and only then can the task implicitly acquire the exclusive read-write lock.

  • Protected functions – Protected functions control an unconditional shared read lock on the protected object. This allows multiple tasks to simultaneously read from the protected object at the same time, but prevents any task to execute any procedure or entry while tasks are calling protected functions.

The following Ada example creates a protected object implementing the shared counter. The protected object in this example implements one procedure and one function. The source code for this program is:

1       with Ada.Text_IO;         use Ada.Text_IO;

2       with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4       procedure Main is

5             Num_Tasks : Positive;

6

7             -- protected object shared by all the tasks

8             protected counter is

9                   procedure update (The_Count : out Natural);

10                   function get_value return Natural;

11             private

12                   count : Natural := 0;

13             end counter;

14

15             protected body counter is

16                   procedure update (The_Count : out Natural) is

17                   begin

18                         count     := count + 1;

19                         The_Count := count;

20                   end update;

21                   function get_value return Natural is

22                   begin

23                         return count;

24                   end get_value;

25             end counter;

26

27             -- Define the task type

28             task type thread is

29                   entry set_id (Id : in Positive);

30             end thread;

31

32             task body thread is

33                   Me       : Positive;

34                   My_Count : Natural;

35             begin

36                   -- accept the set_id entry call from the main task

37                   accept set_id (Id : in Positive) do

38                         Me := Id;

39                   end set_id;

40

41                   counter.update (My_Count);

42                   Put_Line ("Task" & Me'Image & ": counter =" & My_Count'Image);

43             end thread;

44

45       begin

46             Put ("Enter the number of tasks to create: ");

47             Get (Num_Tasks);

48

49             -- declare an inner block in which all the tasks will execute

50             -- the inner block will only complete after all tasks have completed

51             declare

52                   -- create an array of thread objects

53                   pool : array (1 .. Num_Tasks) of thread;

54             begin

55                   for I in pool'Range loop

56                         -- set the id number of each thread object

57                         pool (I).set_id (I);

58                   end loop;

59             end;

60

61      -- output the total after all threads have completed

62      Put_Line

63        ("The final value of the shared counter:" &

64         Natural'Image (counter.get_value));

65

66   end Main;

Ada allows functions, procedures, protected objects and task types to be declared within any function, procedure or task type. This means all items declared in this program are completely local to this program and not “static” as are multiple functions created in the same file in C.

Ada variables are declared by stating the name of the variable, followed by a colon, followed by the type of the variable. This is opposite the order of declaring variables in C where the type is listed first followed by the variable name.

5             Num_Tasks : Positive;

The variable name is Num_Tasks. The type is Positive, which is a pre-defined subtype of the Ada Integer type. Integer is equivalent to the C int type. Positive is an Integer with a minimum possible value of 1 and a maximum possible value of Integer'Last, which is the same as MAX_INT in C. This variable is used to contain the number of tasks the user specifies during the execution of the program.

The protected object named counter is declared in two parts. Protected objects are always declared in two parts. The protected specification defines the public view of the protected methods along with a private view of the data elements in the protected object. The protected body contains the definition of the protected methods, and is not visible to any task calling the protected object.

The protected specification for the counter protected object is:

8             protected counter is

9                   procedure update (The_Count : out Natural);

10                   function get_value return Natural;

11             private

12                   count : Natural := 0;

13             end counter;

There are two methods for this protected object. The procedure named update has one parameter which passes a value of type Natural out to the calling task. Natural is a predefined subtype of Integer with a minimum value of 0. The function named get_value returns a value of the subtype Natural.

In the private section of the protected specification one data element is declared. It is a variable named count. The type of the variable is Natural and it is initialized to 0.

The protected body for the counter protected object is:

15             protected body counter is

16                   procedure update (The_Count : out Natural) is

17                   begin

18                         count     := count + 1;

19                         The_Count := count;

20                   end update;

21                   function get_value return Natural is

22                   begin

23                         return count;

24                   end get_value;

25             end counter;

The update procedure increments count and passes its current value out through the parameter named The_Count.

The function get_value simply returns the value of count. The Ada compiler will issue an error message if the programmer attempts to modify the count data member of the protected object within the execution of the get_value function. Functions are read-only methods in a protected object.

The procedure update implicitly implements a read-write lock and the function get_value implicitly implements a read lock.

The task type named thread is defined in two parts. The first part, the task specification, defines that name of the task type and the direct interfaces to the task type. In this case one task entry is defined. That entry is used to set the task ID.

28             task type thread is

29                   entry set_id (Id : in Positive);

30             end thread;

The task entry implements a direct synchronous communication channel to each instance of the thread type. The entry synchronization scheme is called a Rendezvous. The word "rendezvous" is a French word meaning "a meeting at an agreed time and place". A task calling another task's entry is will suspend until the task declaring the entry accepts the entry call. Similarly, a task accepting an entry will suspend until another task calls that entry. Once both the task calling the entry and the task accepting the entry are in this state for an overlapping time period any data specified in the entry specification is passed between the calling task and the called task. Once the entry has completed both tasks continue to execute concurrently.

The behavior of the task type is defined in the task body.

32             task body thread is

33                   Me       : Positive;

34                   My_Count : Natural;

35             begin

36                   -- accept the set_id entry call from the main task

37                   accept set_id (Id : in Positive) do

38                         Me := Id;

39                   end set_id;

40

41                   counter.update (My_Count);

42                   Put_Line ("Task" & Me'Image & ": counter =" & My_Count'Image);

43             end thread;

The task body declared two local variables. Each instance of the task type has unique instances of these two variables.

The first action taken by the thread task is to accept the set_id entry, passing the Id value from a calling task to this task. The accept statement assigns the value of Id to the task's local variable named Me. Task entries implement a Rendezvous behavior. The thread task will wait at the accept statement until its entry is called by another task. Once the value is passed to the thread task both tasks will the resume concurrent behavior.

The thread task calls the protected object's update procedure, using its local variable My_Count to receive the current value of the counter protected object. Finally, the thread task simply outputs the value it received from the counter.update procedure.

The next “begin” begins the execution of the Main task.

45       begin

46             Put ("Enter the number of tasks to create: ");

47             Get (Num_Tasks);

48

49             -- declare an inner block in which all the tasks will execute

50             -- the inner block will only complete after all tasks have completed

51             declare

52                   -- create an array of thread objects

53                   pool : array (1 .. Num_Tasks) of thread;

54             begin

55                   for I in pool'Range loop

56                         -- set the id number of each thread object

57                         pool (I).set_id (I);

58                   end loop;

59             end;

60

61      -- output the total after all threads have completed

62      Put_Line

63        ("The final value of the shared counter:" &

64         Natural'Image (counter.get_value));

65

66   end Main;

The Main task prompts the user for the number of tasks to create and reads the number entered by the user.

An inner block is created starting at the “declare” reserved word. That inner block has a declarative section in which an array of thread tasks, equal in number to the value entered by the user, is created. The tasks in the array named pool begin executing immediately. Their first responsibility is to accept the set_id entry, so each task waits until its set_id entry is called.

The “for” loop iterates through each element in the pool array assigning the element's index value as the ID for the task.

Immediately after accepting its ID number each task executes counter.update and then outputs the value of the counter passed back through the counter.update procedure.

The inner block will only complete when all the tasks created within the block complete. This produces the same effect as the “join” command in the C example.

After the inner block completes the Main task calls the counter.get_value function and displays the final value of the shared counter. Completion of the inner block automatically frees the array of task objects which were created on the stack at the start of the inner block. No explicit loop to free the task elements is needed or even possible.

Conclusion

While the two programs above achieve the same behavior dealing with allowing multiple threads or tasks to update a shared counter, the C solution contains more opportunities for programmer confusion and error. It also requires a lot more coding by the programmer than the Ada solution.

  • 22 July 2023 at 01:49

Comparing Programming Languages Part 1 Scalar Ranges

 Overview

It is often asserted that all general purpose programming languages can solve the same set of problems. This means that no one programming language has functional advantages over any other programming language.

That assertion is only mostly true, which means it is false.

For example, weakly typed languages can perform and make use of implicit type conversions while strongly typed languages cannot. Strongly typed languages must employ explicit conversions to achieve a similar effect.

The purpose of this article is to begin discussing some of the things that are difficult in one commonly used language and relatively easy in another.

Scalar Ranges

Programming languages derived from Pascal syntax allow scalar types and subtypes to be defined by the programmer, while programming languages derived from C syntax do not allow the programmer to define scalar types or subtypes.

In C++, for example, a class must be declared encapsulating the behavior of a scalar type with a programmer specified range of values. Making a subtype of that class then requires the creation of an inherited class expressing the restrictions distinguishing the subtype.

In C++ enums are encapsulated in a class as illustrated by the following Stack Overflow issue:

How can I implicitly convert an enum to its subset and vice versa in C++?

More precisely, the feature I want is like implicitly convert an enum to its subset enum and vice versa.

The code I wish it working:

enum class Human {

    A = 1,

    B = 2,

};

 

enum class Male {    // subset of Human

    A = Human::A,

};

 

enum class Female {    // subset of Human

    B = Human::B,

};

 

 

// some functions can handle all humans

void human_func(Human h) {

    // ...

}

 

// some only take a subset of humans

void male_func(Male m) {

    // ...

}

 

void female_func(Female m) {

    // ...

}

 

 

// and user only uses values of Human as token

constexpr auto SOMEONE = Human::A;

 

int main() {

    human_func(SOMEONE);  // ok

    male_func(SOMEONE);   // also ok, Human::A implicitly converted to Male

    female_func(SOMEONE); // failed, can't convert Human::A to Female.

}

 

But enum cannot do the conversion. Now I have two options:

// 1. static_assert with template parameter

 

template <Human H>

void female_func() {

    static_assert(H == Human::B);

    // ...

}

 

// 2. manually convert it

 

#define _ENUM_TO_ENUM(e1, e2) \

    static_cast<e2>(static_cast<std::underlying_type_t<decltype(e1)>>(e1))

 

void female_func(_ENUM_TO_ENUM(SOMEONE, Female)) {

    // But this way the compiler does not check if the value is valid.

    // I can put anything in.

    // ...

}

 

 

As is shown above, the concept of a scalar range and subrange is complicated by the need in C++ to express such a type as a class.

One answer provided to this question is

enum class Gender {MALE, FEMALE};

 

struct Human

{

    Gender m_gender;

    Human(Gender g) : m_gender{g}

    {}

    virtual ~Human() = default;

};

 

struct Man : public Human

{

    Man() : Human{Gender::MALE}

    {}

};

struct Woman : public Human

{

    Woman() : Human(Gender::FEMALE)

    {}

};

 

void human_func(const Human & h)

{

    //...

}

void man_func(const Man & m)

{

    //...

}

void woman_func(const Woman & w)

{

    //...

}

 

It is clear that this approach may work for an enum with 2 values, but becomes unusable with an enum containing tens or hundreds of values.

The Ada programming language, on the other hand, uses the concept of scalar ranges and subtypes extensively.

The Character type in Ada is an enumeration type with the range of values expressed as nul .. 'ÿ'. The ASCII characters are a subset of the Character type with value in the range of nul .. del. Within the ASCII characters the upper case characters are the range ‘A’ .. ‘Z’ and the lower characters are the range ‘a’ .. ‘z’.

If the programmer wants to pass only upper case characters as a parameter to a procedure the procedure can be defined as

subtype Upper is range (‘A’ .. ‘Z’);

procedure Upper_Action(U : Upper);

This procedure will only accept characters in the range specified by the subtype Upper.

A function that counts all the upper case characters in a string can be defined as

function Count_Uppers (S : in String) return Natural is

   Count : Natural := 0;

begin

   for value of S loop

      if S in Upper then

         Count := Count + 1;

      end if;

    return Count;

end Count_Uppers;

The Ada samples above exhibit the behavior and usage requested by the person using C++ in the Stack Overflow question above.

The Ada program is not encumbered with the heavy syntax and rules associated with C++ classes.

  • 2 June 2023 at 23:23

Comparison of Bit Array Implementations using C and Ada

 

I found the following programming example in Bit Array in C - Sanfoundry

#include <stdio.h>

#define SIZE (58) /* amount of bits */

#define ARRAY_SIZE(x) (x/8+(!!(x%8)))

char get_bit(char *array, int index);

void toggle_bit(char *array, int index);


void toggle_bit(char *array, int index) {

    array[index / 8] ^= 1 << (index % 8);

}

char get_bit(char *array, int index) {

    return 1 & (array[index / 8] >> (index % 8));

}

int main(void) {

    /* initialize empty array with the right size */

    char x[ARRAY_SIZE(SIZE)] = { 0 };

    int i;

    for (i = 0; i < SIZE; i += 2)

        toggle_bit(x, i);

    toggle_bit(x, 56);

    for (i = 0; i < SIZE; i++)

        printf("%d: %d\n", i, get_bit(x, i));

    return 0;

}

 

The program creates a bit array containing 58 elements. The program works as expected and manages to illustrate very arcane features of the C programming language.

I challenge anybody not familiar with C bit shifting to completely understand how the two void functions named get_bit and toggle_bit actually work.

void toggle_bit(char *array, int index) {

    array[index / 8] ^= 1 << (index % 8);

}

char get_bit(char *array, int index) {

    return 1 & (array[index / 8] >> (index % 8));

}

These functions are examples of the “simplicity” of C programming. While the syntax is compact the semantics of these functions are not. Describing what these two functions do would take several paragraphs of text.

 

As an avid Ada programmer I decided to implement a bit array in Ada and perform the same behaviors on the bit array.

The Ada program is slightly longer, sacrificing compactness for clarity.

-- Ada program to implement a bit array

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

   type bit is range 0 .. 1;

   type index is range 0 .. 57;

   type bit_array is array (index) of bit with

      Component_Size => 1;

   x   : bit_array := (others => 0);

   Idx : index     := 0;

begin

   for I in x'Range loop

      if I mod 2 = 0 then

         toggle_bit (x, I);

      end if;

   end loop;

   toggle_bit (x, 56);


   for I in x'Range loop

      Put_Line (I'Image & ":" & bit'Image (x (I)));

   end loop;

   Put_Line("Size of bit array is" & Integer'Image(X'Size) & " bits.");

   Put_Line("Length of array is" & Integer'Image(X'Length));

end Main;

 

The Ada programming language measures the size of data types in units of bits while the C program must convert bytes to bits in determining its size.

Compare the corresponding Ada and C code sections:

C:

#define SIZE (58) /* amount of bits */

#define ARRAY_SIZE(x) (x/8+(!!(x%8)))

. . .

char x[ARRAY_SIZE(SIZE)] = { 0 };

 

Ada:

   type bit is range 0 .. 1;

   type index is range 0 .. 57;

   type bit_array is array (index) of bit with

      Component_Size => 1;

. . .

x   : bit_array := (others => 0);

 

The Ada code defines an integral type named bit with 0 and 1 as the only valid values. The Ada code then defines an index range to be used in the array type. Knowing the index range prevents buffer overflows in other parts of the program.

The array type bit_array is declared. C provides no means to define array types, only array instances.

In the C example the array named x is defined as an array of type char, but we are not interested in values of type char. We are interested in the bit values within each char element. The size of the char array must be declared to be the number of char elements needed to store 58 bits. Each char element is 8 bits, therefore 8 char elements are needed to store 58 bits.

The corresponding Ada source code is somewhat more readable. A bit_array is an array of 58 elements indexed by the values 0 through 57. The size of each array component is 1, which in Ada terms is 1 bit.

The variable x in the C example is declared to be an array of 8 char data elements. All elements (and all 64 bits) are initialized to 0.

The variable x in the Ada example is declared to be an instance of bit_array, which is declared to be an array of 58 bits. Each element is initialized to 0. Only the 58 bits are initialized to 0.

The C function named toggle_bit is difficult to understand.

void toggle_bit(char *array, int index) {

    array[index / 8] ^= 1 << (index % 8);

}

 

The purpose of this function is to identify the bit specified by the index parameter and toggle the bit. If the bit is 1 then set it to zero. If the bit is 0 then set it to 1.

The Ada procedure named toggle_bit performs the same behavior with a somewhat more verbose, but also clearer syntax.

   procedure toggle_bit (arr : in out bit_array; Idx : index) is

   begin

      if arr (Idx) = 1 then

         arr (Idx) := 0;

      else

         arr (Idx) := 1;

      end if;

   end toggle_bit;

 

Note that the bits in the bit array are indexed with the same syntax used to index any other Ada array. No special syntax is needed. The compiler writes all the low level bit shifting for the programmer, eliminating the need for an explicit get_bit procedure as found in the C example. The Ada version clearly states toggle logic. If the current value of the bit indexed by Idx is 1 then assign 0 to that bit, otherwise assign 1 to that bit.

Also note the obscurity of the C syntax for passing an array to a function. The array is not actually passed to the function. The name of the array is passed as a pointer to the first element of the array, thus the parameter used to “pass” the array is a pointer to char rather than an array. A pointer to char may be a pointer to the first element of an array of char or it may be a pointer to a char which is not a member of an array of char. C requires the programmer to know whether or not the parameter points to an array. The C syntax also does not specify whether or not the actual parameter passed to this function may be modified by the function.

The Ada procedure specifies the parameter arr is an instance of bit_array. Furthermore, the passing mode “in out” specifies that the value of the array is used and modified by the procedure, specifying that the actual parameter passed to this procedure may be modified by the procedure.

Outputs:

The output of the C program is:

0: 1

1: 0

2: 1

3: 0

4: 1

5: 0

6: 1

7: 0

8: 1

9: 0

10: 1

11: 0

12: 1

13: 0

14: 1

15: 0

16: 1

17: 0

18: 1

19: 0

20: 1

21: 0

22: 1

23: 0

24: 1

25: 0

26: 1

27: 0

28: 1

29: 0

30: 1

31: 0

32: 1

33: 0

34: 1

35: 0

36: 1

37: 0

38: 1

39: 0

40: 1

41: 0

42: 1

43: 0

44: 1

45: 0

46: 1

47: 0

48: 1

49: 0

50: 1

51: 0

52: 1

53: 0

54: 1

55: 0

56: 0

57: 0

 

The output of the Ada program is:

 0: 1

 1: 0

 2: 1

 3: 0

 4: 1

 5: 0

 6: 1

 7: 0

 8: 1

 9: 0

 10: 1

 11: 0

 12: 1

 13: 0

 14: 1

 15: 0

 16: 1

 17: 0

 18: 1

 19: 0

 20: 1

 21: 0

 22: 1

 23: 0

 24: 1

 25: 0

 26: 1

 27: 0

 28: 1

 29: 0

 30: 1

 31: 0

 32: 1

 33: 0

 34: 1

 35: 0

 36: 1

 37: 0

 38: 1

 39: 0

 40: 1

 41: 0

 42: 1

 43: 0

 44: 1

 45: 0

 46: 1

 47: 0

 48: 1

 49: 0

 50: 1

 51: 0

 52: 1

 53: 0

 54: 1

 55: 0

 56: 0

 57: 0

Size of bit array is 64 bits.

Length of array is 58

 

  • 22 April 2023 at 19:27

Poor Quality C Programming Examples

 

C is hard enough without low quality programming examples

I recently read through some of the C programming examples from Sanfoundry. Some of the examples are well constructed while others are very poorly constructed.

One of the bad examples I read is found under the heading of Simple C Programs. The example purports to show how to count the number of vowels and consonants in an input sentence.

The source code for the example follows.

/*

 * C program to read a sentence and count the total number of vowels

 * and consonants in the sentence.

 */

#include <stdio.h>

 

void main()

{

    char sentence[80];

    int i, vowels = 0, consonants = 0, special = 0;

 

    printf("Enter a sentence \n");

    gets(sentence);

    for (i = 0; sentence[i] != '\0'; i++)

    {

        if ((sentence[i] == 'a' || sentence[i] == 'e' || sentence[i] ==

        'i' || sentence[i] == 'o' || sentence[i] == 'u') ||

        (sentence[i] == 'A' || sentence[i] == 'E' || sentence[i] ==

        'I' || sentence[i] == 'O' || sentence[i] == 'U'))

        {

            vowels = vowels + 1;

        }

        else

        {

            consonants = consonants + 1;

        }

        if (sentence[i] =='\t' ||sentence[i] =='\0' || sentence[i] ==' ')

        {

            special = special + 1;

        }

    }

    consonants = consonants - special;

    printf("No. of vowels in %s = %d\n", sentence, vowels);

    printf("No. of consonants in %s = %d\n", sentence, consonants);

}

 

This program does not actually identify a sentence. Instead it merely reads a single input line from stdin. The first conditional carefully identifies the values of the vowels and then assumes anything not a vowel is a consonant. A second conditional makes a weak attempt to find “special” characters that are not letters, but this conditional only looks for tabs, spaces and end of string null characters. It completely ignores punctuation and numeric digits. Sentences often contain punctuation and numeric digits, which are neither vowels nor consonants. The error shows in an inflated count of consonants when an input string contains punctuation and/or numeric digits.

The example program is poorly designed and does not fulfill the requirements expressed on the page C Program to Count the Number of Vowels and Consonants in a Sentence - Sanfoundry.

I wrote an Ada program to achieve the same stated goals. This program uses approximately the same number of lines of source code as the C program while avoiding the problem of miscounting consonants.

-- Ada program to read a line of text and count the number of vowels

-- and consonants in the text

 

with Ada.Text_IO; use Ada.Text_IO;

 

procedure Main is

   subtype Letter is Character with

        Static_Predicate => Letter in 'a' .. 'z' | 'A' .. 'Z';

   subtype Vowel is Character with

     Static_Predicate => Vowel in 'a' | 'e' | 'i' | 'o' | 'u' |

       'A' | 'E' | 'I' | 'O' | 'U';

 

   Line       : String (1 .. 80);

   vowels     : Natural := 0;

   consonants : Natural := 0;

   Length     : Natural;

begin

   Put_Line ("Enter a sentence:");

   Get_Line (Item => Line, Last => Length);

   for I in 1 .. Length loop

      if Line (I) in Letter then

         if Line (I) in Vowel then

            vowels := vowels + 1;

         else

            consonants := consonants + 1;

         end if;

      end if;

   end loop;

   Put_Line

     ("Number of vowels in " & Line (1 .. Length) & " is" & vowels'Image);

   Put_Line

     ("Number of consonants in " & Line (1 .. Length) & " is" &

      consonants'Image);

end Main;

 

The Ada program explicitly defines a subtype of Character containing only English letters. It also defines a subtype of Character defining only the vowels defined in the C program.

The first conditional in the Ada program filters out all characters that are not letters. Within that conditional block the current character is tested for being a vowel. If it is not a vowel then it can only be a consonant. Both the vowel count and the consonant count are correct because the program selected only letters and filtered out all non-letter characters.

C language philosophy

The C language expects the programmer to perform all error checking while only providing very primitive means of specifying the correct data conditions. Historically C has concentrated its design on execution speed at the expense of programmer effort.

Ada language philosophy

The Ada language provides syntax tools to define subtypes of a data type based upon either a range of values or set of values. The Ada example above defines two subtypes of the predefined type Character. One subtype defines the set of all lower case and upper case letters. The other subtype defines the set of lower case and upper case letters identified as vowels.

Each of these subtypes is expressed in a very compact manner in a single expression.

These subtypes express exactly what is a letter and what is a vowel.

Conclusion

The C program never specifies what is a letter. The result is a latent error in program logic which most beginning C students would be unlikely to identify. Eliminating this error would require either a laborious list of letters in C or a list of the numeric ranges representing lower case letters and upper case letters. The first option greatly obscures the meaning of the filtering by its complexity. The second option greatly obscures the meaning of the filtering by using the numeric representation of the letters in a compound conditional expression such as

If ((sentence[i] >= 65 && sentence[i] <= 90) || (sentence[i] >= 97 && sentence[i] <= 122))

The Ada equivalent is accomplished by defining the set of values constituting a letter

subtype Letter is Character with

        Static_Predicate => Letter in 'a' .. 'z' | 'A' .. 'Z';

 

Followed by a simple conditional

if Line (I) in Letter then

The Ada “in” operator returns True if Line (I) is a member of the set of values defined for Letter and False if Line (I) is not a member of the set of values define for Letter.

The set of letters is defined as all the characters in the range starting at ‘a’ and ending at ‘z’ and all the characters in the range starting at ‘A’ and ending at ‘Z’. While this set defines the same values as the C conditional example above, it does so in a very clear manner understandable by programmers of all levels of experience, without resorting to a table of values mapping characters to numeric representations.

The Ada program clearly specifies all the values needed to count vowels and consonants, thereby eliminating the latent defect present in the C program.

  • 5 April 2023 at 05:08

Create a Reversed Copy of a String

 A recent question on Stack Overflow concerned creation of a C program to create and print a reverse copy of a string. Thus, for example, the string “hello” would be copied to another string and would contain “olleh”.

C versions

While this is clearly a beginner level problem, the difficulties encountered by the person submitting the question illustrate how learning C can be a struggle.

The source code posted by the author of the question is:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#define ARR_SIZE 50

int main()
{
 char string[ARR_SIZE];
 printf("Enter char array!\n");
 fgets(string, ARR_SIZE, stdin);

 string[strlen(string) - 1] = '\0';
 int length = (strlen(string) - 1);
 int length2 = (strlen(string) - 1);

 printf("%s\t%d\n", string, length);

 for (int i = 0; i <= length; i++)
 {
 printf("INDEX = %d CHAR = %c\n", i, string[i]);
 }
 
 printf("%d", length2);
 char copy[ARR_SIZE];
 
 for (int i = 0; i <= length2; i++)
 {
 copy[i] = string[length];
 length--;
 }

 


 printf("\n%s", copy);
}

 

There are a number of beginner mistakes in this code example. One answer attempted to correct the most obvious errors with the following result:

#include <stdio.h>
#include <string.h>
// remove unneeded headers

#define ARR_SIZE 50

int main(void)
{
  char string[ARR_SIZE];
  printf("Enter char array!\n");
  fgets(string, ARR_SIZE, stdin);

  string[strlen(string) - 1] = '\0';
  // remove the -1 on the string length calculation, the NUL terminator is not
  // included in strlen's return value
  int length = strlen(string);
  // no sense in calling strlen twice
  int length2 = length;

  // fixed `length` now prints the correct length
  printf("%s\t%d\n", string, length);

  // change from <= to <. The array indices where the characters live are
  // [0, length-1].
  for (int i = 0; i < length; i++)
  {
    printf("INDEX = %d CHAR = %c\n", i, string[i]);
  }
 
  // fixed `length2` now prints the correct length
  printf("%d", length2);
  char copy[ARR_SIZE];
 
  for (int i = 0; i < length2; i++)
  {
    // last character in `string` lives at the `length`-1 index
    copy[i] = string[length-1];
    length--;
  }

  // `length2` is the index after the last char in `copy`, this needs
  // to be NUL terminated.
  copy[length2] = '\0';

  // prints the reversed string
  printf("\n%s", copy);
}

 

These suggestions do produce a working program, but it has some weaknesses inherent in the C language handling of strings. The biggest weakness is the effort needed to deal with determining how long the string is. C strings are terminated with a null character. The example above uses the strlen function to find the position of the null character.

A second answer tried to offer a more compact solution, although it does omit the necessary header files. The author of this example suggested that a proper solution should define and use a function. Note that this function is highly compact, uncommented, and extremely terse. In other words it contains all the elements commonly associated with C programming.

char *copyreverse(char *dest, const char *src)
{
  size_t len = strlen(src);
  const char *end = src + len - !!len;
  char *wrk = dest;
  while(len--)
    *wrk++ = *end--;
  *wrk = 0;
  return dest;
}
int main()
{
  char dest[10];
  char *src = "hello";
  printf("`%s` reversed `%s`\n", src, copyreverse(dest, src));
}

 

In this case we see a function named copyreverse and a function named main in the same file. In C this places the two functions in the same file scope, allowing copyreverse to be visible to the implementation of main. C does not allow a function to be implemented within the scope of another function.

This terse solution has typical C problems. The copyreverse function takes two parameters, a pointer to character named dest and a pointer to character named src. Only the programmer know that these pointers should point to the start of an array of characters and not a simple character. There is no check in the program to ensure that the dest parameter points to an array large enough to hold all the characters contained in the array pointed to by the parameter src. This failure to check sizes opens the opportunity for a buffer overflow.

Another part of the simplification of this program is its lack of ability to read the string from standard input, thus making the program unnecessarily compact.

Ada versions

Next I offer a couple of solutions created in the Ada programming language.

The first Ada example creates a function using the Ada.Strings.Unbounded package which provides an append procedure for unbounded strings. Use of unbounded strings prevents any buffer overflow problems.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

procedure Main is
  Result : Unbounded_String := Null_Unbounded_String;
  Input : String (1 .. 80);
  Length : Natural;
begin
  Put ("Enter a string: ");
  Get_Line (Item => Input, Last => Length);
  Put_Line (Input (1 .. Length));
  for C of reverse Input (1 .. Length) loop
    Append (Result, C);
  end loop;
  Put_Line (To_String (Result));
end Main;

 

The Get_Line procedure reads the string from standard input and places it into a string named Input. The Get_Line function passes out the modified string as well as a parameter indicating the index of the last character of the useful information in the string. The Put_Line procedure simply writes the input data to standard output. The data to be output is in the slice of the Input variable which was filled by Put_Line.  The for loop scans through the slice of Input in reverse order, appending each value to the Unbounded_String named Result. When the loop finishes Result will contain the data in Input (1 .. Length), but in reverse order.

The final Put_Line procedure call simply converts the Unbounded_String Result to a corresponding String value and outputs that String value.

A second Ada example avoids the use of the package Ada.Strings.Unbounded and does the string copy directly to another Ada string.

with Ada.Text_IO; use Ada.Text_IO;

procedure main_2 is
   function reverse_str (src : in String) return String is
     Result : String (src'Range);
     r_idx : Positive := Result'First;
   begin
     for value of reverse src loop
       Result (r_idx) := value;
       r_idx := r_idx + 1;
     end loop;
   return Result;
  end reverse_str;

  Input : String (1 .. 80);
  Length : Natural;
begin
  Put ("enter a string: ");
  Get_Line (Input, Length);
  Put_Line (Input (1 .. Length));
  Put_Line (reverse_str (Input (1 .. Length)));
end main_2;

 

The function named reverse_str is defined within the procedure main_2. Reverse_str takes one String parameter named src and returns a String. Reverse_str creates a String variable named Result. Result is created to have the same index range as src, and therefore has exactly the same size as src, eliminating any possibility of overflow. The variable r_idx is an instance of the subtype Positive and is initialized to the first index value of Result. The for loop iterates through src in reverse assigning each value of src to the element in Result indexed by the variable r_idx. The variable r_idx is then incremented and the loop continues until all elements of src have been processed. Result is then returned by the function reverse_str.

The body of main_2 prompts for a string to be input from standard input, reads up to 80 characters from standard input and places the data in the variable Input. The value of the string Input (1 .. Length) is output and on the next output line the value of the reverse of Input (1 .. Length) is output.

While not as terse as the second C solution the second Ada version is highly readable and does not require the programmer to provide a destination string parameter of possibly dubious size as is required in the C program. The Ada version also does not require the src string to be scanned to determine its size followed by an obscure expression seen in the C example to deal with a possible empty string. Instead the Ada range syntax defines a range where the first index is greater than the last index to be an empty range. For instance, if the users passes an empty string to the Ada program the notation Input (1 .. Length) resolves to Input ( 1 .. 0), which is an empty string. The for loop then does not iterate through the values because the first index exceeds the last index and Result, which is created with the expression Result : String := (src’Range) is defined as an empty string. Result is returned without being modified in the for loop and no buffer overflow occurs.

The Ada versions are both simpler because of the differences between Ada strings and C strings and also because Ada arrays, of which strings are an example, can easily be sliced.

The Ada versions both avoid the possibility of array overflows while the second C version does not.

The Ada versions avoid any pointer manipulation syntax while the second C example performs all array element processing using explicit pointer manipulations.

 

  • 4 November 2021 at 04:24

Is Ada truly verbose?

 

People who prefer programming language syntax derived from the C language have often argued that the Ada language is verbose because it uses entire words rather than punctuation.

Meaning

C Syntax

Ada Syntax

Begin a block

{

begin, loop

End a block

}

end, end if, end loop, end subprogram name, end task name, end protected object name, end record

Declare a function taking a single integer parameter and returning an integer value

int foo (int x);

function foo (x : integer) return integer;

Declare a 10 element integer array indexed with the values 0 through 9

int a [10];

a : array (0..9) of integer;

Write a “for” loop to sum all the elements in array a declared above.

for (int I = 0; I < 10; i++)

{

   sum += a [i];

}

for I in a’range loop

   sum := sum + a (i);

end loop;

 

Of course, not everything in Ada takes more lines of code or more typing than the equivalent program in C or C++. For instance, the following C++ program is taken from the CodesCracker website. Its purpose is to convert a user input of a hexadecimal value into the corresponding decimal value.

/* C++ Program - Hexadecimal to Decimal Conversion */            

#include<iostream.h>

#include<stdlib.h>

#include<conio.h>

#include<math.h>

unsigned long convtodecnum(char hex[]);

void main()

{

    clrscr();

    unsigned long decnum;

    char hex[9];     // 8 characters for 32-bit Hexadecimal Number and one for ' '   

    cout<<" Enter 32-bit Hexadecimal Number : ";

    cin>>hex;

    decnum = convtodecnum(hex);

    cout<<"Value in Decimal Number is "<<decnum<<"\n";

    getch();

}

unsigned long convtodecnum(char hex[])

{

    char *hexstr;

    int length = 0;

    const int base = 16;     // Base of Hexadecimal Number

    unsigned long decnum = 0;

    int i;

    // Now Find the length of Hexadecimal Number

    for (hexstr = hex; *hexstr != '\0'; hexstr++)

    {

       length++;

    }

    // Now Find Hexadecimal Number

    hexstr = hex;

    for (i = 0; *hexstr != '\0' && i < length; i++, hexstr++)

    {

       // Compare *hexstr with ASCII values

       if (*hexstr >= 48 && *hexstr <= 57)   // is *hexstr Between 0-9

       {

           decnum += (((int)(*hexstr)) - 48) * pow(base, length - i - 1);

       }

       else if ((*hexstr >= 65 && *hexstr <= 70))   // is *hexstr Between A-F

       {

           decnum += (((int)(*hexstr)) - 55) * pow(base, length - i - 1);

       }

       else if (*hexstr >= 97 && *hexstr <= 102)   // is *hexstr Between a-f

       {

           decnum += (((int)(*hexstr)) - 87) * pow(base, length - i - 1);

       }

       else

       {

           cout<<"Invalid Hexadecimal Number \n";

       }

    }

    return decnum;

}

 

An Ada program handling the same input and results in the same output is:

-- Convert Hexadecimal string to decimal value

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

   type Unsigned_Long is mod 2**32;

   Decimal : Unsigned_Long;

begin

   Put ("Enter a 32 bit hexadecimal number: ");

   Decimal := Unsigned_Long'Value ("16#" & Get_Line & "#");

   Put_Line (Decimal'Image);

exception

   when Constraint_Error =>

      Put_Line ("Input value is not a valid 32 bit hexadecimal number.");

end Main;

 

Ada has the built-in capability to handle literal numbers represented by base 2 through base 16. The base 16 value FF is represented as 16#FF#. Ada accepts either upper or lower case representation of the hexadecimal values A through F. The program simply appends the 16# to the entered hexadecimal digits and then appends # to the end of the string. The integer’value built in attribute converts a string representation of an integer to its corresponding integer value.

The message created in response to the exception Constraint_Error is raised if the user enters a value more than 2^32 or if one of the digits is not in the range of 0 through 9 or A through F.

In this case the Ada program appears to be far less verbose than the C++ program.

  • 22 October 2020 at 04:16

Security Snippet Number 2

This security snippet deals with CWE-190 “Integer Overflow or Wraparound” described in https://cwe.mitre.org/data/definitions/190.html
The problem description for this weakness enumeration states:
An integer overflow or wraparound occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may wrap to become a very small or negative number. While this may be intended behavior in circumstances that rely on wrapping, it can have security consequences if the wrap is unexpected. This is especially the case if the integer overflow can be triggered using user-supplied inputs. This becomes security-critical when the result is used to control looping, make a security decision, or determine the offset or size in behaviors such as memory allocation, copying, concatenation, etc.
This problem is prevalent in languages exhibiting structural type definitions rather than nominative type definitions for numeric types[1]. Languages such as C which use structural type definitions provide implicit type conversions between numeric types. Furthermore, structural type definitions of numeric types do not allow the programmer to define a type with a programmer-defined range of valid values. Integer types, for instance, generally come in 8 bit, 16 bit, 32 bit and 64 bit types. There are no provisions in C to define an 8 bit type with a range of -10 through 10. The closest one can do is use a signed char type which provides a range of values from -128 through 127. Furthermore, since C uses only structural information to determine a numeric type integer types experience wrap-around when overflowing or underflowing values.
Using nominative types for numeric data types also allows the specification of a particular data range for a type. Thus, two different types using the same bit representation, such as 8 bits, can be kept separate.
The following example of C code is given in CWE-190 as an example of wrap-around issues:
#define JAN 1

#define FEB 2

#define MAR 3


short getMonthlySales(int month) {...}


float calculateRevenueForQuarter(short quarterSold) {...}


int determineFirstQuarterRevenue() {


// Variable for sales revenue for the quarter

float quarterRevenue = 0.0f;


short JanSold = getMonthlySales(JAN); /* Get sales in January */

short FebSold = getMonthlySales(FEB); /* Get sales in February */

short MarSold = getMonthlySales(MAR); /* Get sales in March */


// Calculate quarterly total

short quarterSold = JanSold + FebSold + MarSold;


// Calculate the total revenue for the quarter

quarterRevenue = calculateRevenueForQuarter(quarterSold);


saveFirstQuarterRevenue(quarterRevenue);


return 0;

}

This code actually exhibits many faults, as well as a potential wrap-around.
·         The macros defining JAN, FEB, and MAR evaluate to integer values which are interpreted as month numbers by the function getMonthlySales. There is no assurance that a value greater than 12 cannot be passed to the function, resulting in erroneous behavior.
·         Each month’s sales are limited to a maximum of 32767 which may be too small for the monthly revenue for a business. Furthermore, the total of three months sales numbers is also restricted to a maximum of 32767.
A more correct implementation using Ada, which provides nominative numeric types is:
type Months is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);


type Sales_Count is range 0..2**31;


function getMonthlySales(Month : Months)return Sales_Count;


function calculateRevenueForQuarter(Count : Sales_Count) return float;


procedure determineFirstQuarterRevenue is


   JanSold : Sales_Count := getMonthlySales(Jan);

   FebSold : Sales_Count := getMonthlySales(Feb);

   MarSold : Sales_Count := getMonthlySales(Mar);


   Quarter_Sold : Sales_Count;


begin


   Quarter_Sold := JanSold + FebSold + MarSold;


   saveFirstQuarterRevenue(calulateRevenueForQuarter(Quarter_Sold));


end determineFirstQuarterRevenue;


The Ada version implementation shown above defines values for all months and requires the function getMonthlySales to use a parameter of the enumeration type Months, not simply an integer. The C version allows any int value to be passed to the function.
The type Sales_Count is defined to hold any value from 0 through 2^31 (2,147,483,648). Note that this also ensures that a negative sales count cannot be reported. The type Sales_Count will not exhibit wrap-around.
Use of enumerations in C will not provide the same benefits as the Ada enumerated type in the example above. Ada enumerated types are a separate type not implicitly convertible to an integer type, while C enums are simply aliases for int values. The parameter type for the function must still be an int, which does not restrict the parameter values to a set of valid values.
  • 2 February 2019 at 00:59

Ada 2012 Static Predicates


1.     Subtypes

Every Ada type is associated with a subtype. A subtype is a subset of the base type, commonly with some restriction in the set of values valid for the subtype. For instance, Ada provides a pre-defined type named Integer. Integer is a signed type representing all the values in the range of -2**31..2**31 – 1. Ada also provides two pre-defined subtypes of Integer: Natural, which is an integer with a minimum value of 0, and Positive which is an integer with a minimum value of 1. Every instance of Natural or Positive is also an instance of Integer. This capability to define subtypes has been a part of Ada since the first Ada language standard in 1983.
While this subtype capability has been very useful, it had some restrictions. Subtype ranges were always restricted to contiguous ranges. For instance, the syntax for defining the Natural subtype mentioned above is:
subtype Natural is Integer range 0..Integer’Last;

 Integer’Last evaluates to the highest valid value defined for the type Integer.

2.     Static Predicates

Ada 2012 added new capabilities to the definition of subtypes. One of those capabilities is the definition of static predicates. Static predicates allow the definition of statically defined non-contiguous sets of values.
For instance, a subtype of the pre-defined type Character can be defined to contain only the letters in the ASCII subset of Character:
subtype Letters is Character with Static_Predicate => ‘A’..’Z’ | ‘a’..’z’;

Notice that the subtype Letters contains two contiguous ranges. One range includes the upper case letters and the other contains the lower case letters. Similarly one can define a subtype with no contiguous ranges of values such as:
subtype Vowels is Character with Static_Predicate => ‘A’ | ‘E’ | ‘I’ | ‘O’ | ‘U’ | ‘a’ | ‘e’ | ‘i’ | ‘o’ | ’u’;

2.1. Example

Following are two programs used to calculate the number of vowels and consonants in a string read from standard input. The C code is taken from https://www.sanfoundry.com/c-program-count-number-vowels-consonants-sentence/
Both programs achieve the same goal with some problems in the C code dealing with punctuation and tab characters.

2.1.1.     C Code

/*

 * C program to read a sentence and count the total number of vowels

 * and consonants in the sentence.

 */

#include <stdio.h>


void main()

{

    char sentence[80];

    int i, vowels = 0, consonants = 0, special = 0;


    printf("Enter a sentence \n");

    gets(sentence);

    for (i = 0; sentence[i] != '\0'; i++)

    {

        if ((sentence[i] == 'a' || sentence[i] == 'e' || sentence[i] ==

        'i' || sentence[i] == 'o' || sentence[i] == 'u') ||

        (sentence[i] == 'A' || sentence[i] == 'E' || sentence[i] ==

        'I' || sentence[i] == 'O' || sentence[i] == 'U'))

        {

            vowels = vowels + 1;

        }

        else

        {

            consonants = consonants + 1;

        }

        if (sentence[i] =='t' ||sentence[i] =='\0' || sentence[i] ==' ')

        {

            special = special + 1;

        }

    }

    consonants = consonants - special;

    printf("No. of vowels in %s = %d\n", sentence, vowels);

    printf("No. of consonants in %s = %d\n", sentence, consonants);

}

2.1.2.     Ada 2012 Code

-----------------------------------------------------------------------

-- Count the number of vowels and consonants in an input string

-----------------------------------------------------------------------

with Ada.Text_IO; use Ada.Text_Io;


procedure Count_Letters is

   subtype Vowels is Character with

     Static_Predicate => Vowels in

       'A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u';

   subtype Letters is Character with

     Static_Predicate => Letters in 'A'..'Z' | 'a'..'z';

   Msg         : String(1..1024);

   Length      : Natural;

   Cons_Count  : Natural := 0;

   Vowel_Count : Natural := 0;

begin

   Put("Enter phrase: ");

   Get_Line(Msg, Length);

   for Char of Msg(1..Length) loop

      if Char in Vowels then

         Vowel_Count := Vowel_Count + 1;

      elsif Char in Letters then

         Cons_Count := Cons_Count + 1;

      end if;

   end loop;

   Put_Line('"' & Msg(1..Length) & '"' & " contains" & Vowel_Count'Image &

              " vowels and" & Cons_Count'Image & " consonants.");

end Count_Letters;


The subtypes Vowels and Letters are used to define sets of values. The “if” statement tests each character of the input string for membership in one set or the other. All characters not in either set, such as spaces or punctuation, are not counted. Note that the C version of the program counts all characters which are not vowels then subtracts specified non-letter characters. The C code will actually count punctuation as consonants because it does not include those characters in its list of special characters. On the other hand the Ada code contains positive definitions of the characters to be counted and only counts the specified characters. This ability to unambiguously define the set of characters to count eliminates a set of errors hidden in the C code.

  • 5 October 2018 at 16:06

Comparison of Array Based Stacks in C and Ada


Comparison of Array Based Stacks in C and Ada

The stack is one of the simplest data structures. Array-based stacks can be implemented in all languages supporting arrays, even including early versions of Fortran which did not support pointers.

This comparison is based upon C code published at http://www-cs.ccny.cuny.edu/~peter/dstest.html
The C code published at http://www-cs.ccny.cuny.edu/~peter/dstest.html supports the book Advanced Data Structures authored by Peter Braß and published by Cambridge University Press.


The Ada code uses Ada2012, which added aspect specifications, pre and post conditions, type invariants and subtype predicates to the Ada language. Descriptions of these and other features added in the Ada 2012 version are available at The Ada 2012 Rationale

Array Stack

The C code for Array Stack is shown below.

Example 1 ArrayStack.c
#include <stdio.h>

#include <stdlib.h>


typedef int item_t;


typedef struct {item_t *base; item_t *top; int size;} stack_t;


stack_t *create_stack(int size)

{   stack_t *st;

    st = (stack_t *) malloc( sizeof(stack_t) );

    st->base = (item_t *) malloc( size * sizeof(item_t) );

    st->size = size;

    st->top = st->base;

    return( st );

}


int stack_empty(stack_t *st)

{   return( st->base == st->top );

}


int push( item_t x, stack_t *st)

{   if ( st->top < st->base + st->size )

    {   *(st->top) = x; st->top += 1;  return( 0 );

    }

    else

       return( -1 );

}


item_t pop(stack_t *st)

{   st->top -= 1;

    return( *(st->top) );

}


item_t top_element(stack_t *st)

{   return( *(st->top -1) );

}


void remove_stack(stack_t *st)

{   free( st->base );

    free( st );

}




int main()

{  stack_t *st;

   char nextop;

   st = create_stack(50);

   printf("Made Array-Based Stack of size 50\n");

   while( (nextop = getchar())!= 'q' )

   { if( nextop == 'i' )

     { int insitem;

       scanf(" %d", &insitem);

       push( insitem, st );

       printf(" pushed %d. The current top item is %d\n", insitem,

           top_element(st) );

     } 

     if( nextop == 'd' )

     { int de_item;

       getchar();

       de_item = pop(st);

       printf("  popped item %d", de_item);

       if( stack_empty(st) )

         printf(" the stack is now empty\n");

       else

         printf(" the top element is now %d\n",  top_element(st) );


     }

     if( nextop == '?' )

     { getchar();

       if( stack_empty(st) )

         printf("the stack is empty\n");

       else

         printf("the top element is %d\n", top_element(st) );

     }

    

   }

   remove_stack(st);

   printf(" removed stack\n");

   return(0);

}

The author claims that his book is not a text on Object Oriented Programming, but instead a text on advanced data structures.  None of the examples in the URL referenced above employ C header files. I assume that was done to simplify publication, placing both the data structure, including its associated functions, and a main program to demonstrate the structure, in a single file.

Issues with ArrayStack.c

While the author could have used array indexing to access the values of the array at the heart of the stack_t struct, he chose to perform pointer arithmetic directly. From a style point of view it is often preferred to use array indexing to access the elements of an array.

The push function

Example 2 push function
int push( item_t x, stack_t *st)

{   if ( st->top < st->base + st->size )

    {   *(st->top) = x; st->top += 1;  return( 0 );

    }

    else

       return( -1 );

}

Note that this code only actually pushes a value onto the stack if the stack is not full. This is a correct behavior. When the array is full this function returns -1.
Example 3 Calling push
   while( (nextop = getchar())!= 'q' )

   { if( nextop == 'i' )

     { int insitem;

       scanf(" %d", &insitem);

       push( insitem, st );

       printf(" pushed %d. The current top item is %d\n", insitem,

           top_element(st) );

     } 


The example  of calling the push function ignores the return value. This means that the push operation fails silently. The user is given no indication that the push fails. In fact, the printf statement following the push function call tells the user that the push succeeds, whether it does nor not.

The pop function

Example 4 The pop function
item_t pop(stack_t *st)

{   st->top -= 1;

    return( *(st->top) );

}

Please note that the pop function does not check for an empty stack. Logically, it is erroneous to pop a value from an empty stack. This function simply decrements the pointer st->top and returns whatever value is at that memory location. Since no check is made for an empty stack, the result can be to return the value at some memory location before the first element of the dynamically allocated array pointed to by st->base. This is a form of buffer overflow.
The main function calls the pop function before checking if the stack is empty. Checking the state of the stack before calling pop would be the correct behavior. Nothing in the main function prevents pop from being called when the stack is empty.
     if( nextop == 'd' )

     { int de_item;

       getchar();

       de_item = pop(st);

       printf("  popped item %d", de_item);

       if( stack_empty(st) )

         printf(" the stack is now empty\n");

       else

         printf(" the top element is now %d\n",  top_element(st) );


     }

If the pop function is called several times when the stack is empty the st->top pointer will continue to be decremented. If, after that the push function is called, the pushed value will be assigned to a memory location before the first element of the array, corrupting memory outside of the array.

The top_element function

A somewhat milder version of the problem with the pop function is present in the top_element function. This function returns the value of in the address one less than the current st-top pointer.
Example 5 The top_element function
item_t top_element(stack_t *st)

{   return( *(st->top -1) );

}

As you can see, the top_element function does not check for an empty stack.

The stack_empty function

Example 6 The stack_empty function
int stack_empty(stack_t *st)

{   return( st->base == st->top );

}

Note that this function will only return True when st->top points to the first element of the allocated array. It will return False when st->top points to any memory location before the beginning of the array. Since the function pop can move the pointer st->top to memory locations before the beginning of the array this function is highly unreliable.

Conclusions concerning ArrayStack.c

While this code may convey the general approach to implementing an array-based stack data structure, the actual implementation is faulty in many ways. All of the faults are associated with the primitive implementation of arrays in the C language. C arrays are simply a block of memory accessed by pointers. C provides no array bounds checking.
The code for this implementation may execute very efficiently, but it does no good when the code is executing erroneously.

Bounded_Stack

The Ada code for an unbounded stack type is shown below. The following example implements a generic stack using an array with the size of the array determined at compile time.
Ada requires a package to implement both a specification, which is analogous to a C header file, and a body, which contains the implementation of the procedures and functions declared in the specification.

The file bounded_stack.ads

The file bounded_stack.ads contains the interface specification for a generic stack based upon an array.
generic

   type Element_Type is private;

   Default_Value : Element_Type;

package Bounded_Stack is

   type Stack(Size : Positive) is tagged private;

   function Is_Empty(Item : Stack) return Boolean;

   function Is_Full(Item : Stack) return Boolean;

   procedure Push(Item : in out Stack; Value : in Element_Type) with

     Pre => not Is_Full(Item),

     Post => not Is_Empty(Item);

   procedure Pop(Item : in out Stack; Value : out Element_Type) with

     Pre => not Is_Empty(Item),

     Post => not Is_Full(Item);

   function Top(Item : in Stack) return Element_Type with

     Pre => not Is_Empty(Item);

   procedure Clear(Item : in out Stack) with

     Post => Is_Empty(Item);

private

   type Buffer is array(Positive range <>) of Element_Type;

   type Stack(Size : Positive) is tagged record

      Buf   : Buffer(1..Size) := (Others => Default_Value);

      Index : Positive := 1;

      Count : Natural  := 0;

   end record;

end Bounded_Stack;

This is a generic package, as indicated by the reserved word “generic” at the start of the file. This package has two generic parameters; Element_Type, which designates any non-limited type, and Default_Value, which is used to initialize every instance of the stack with a properly define default value.
Declaring the type Stack to be private means that the details of the type are hidden from any calling subprogram or task. The subprogram declarations following the type declaration, and preceding the reserved word “private” are the only means given to manipulate the stack.

The procedure Push

Example 7 The procedure Push
   procedure Push(Item : in out Stack; Value : in Element_Type) with

     Pre => not Is_Full(Item),

     Post => not Is_Empty(Item);

The declaration of the procedure Push contains a lot of information.
The parameter Item must be an instance of the type Stack. The parameter Item will be modified during the execution of the procedure Push. The parameter Value is an instance of Element_Type, and it will not be modified during execution of the procedure Push.
The procedure Push has a simple pre-condition, namely that the instance of Stack passed to this procedure cannot be full before calling Push.
The procedure Push has a simple post-condition, namely that the instance of Stack passed to the procedure Push will not be empty upon completion of the procedure Push.
The pre-condition and the post-condition are enforced by the compiler.

The procedure Pop

Example 8 The procedure Pop
   procedure Pop(Item : in out Stack; Value : out Element_Type) with

     Pre => not Is_Empty(Item),

     Post => not Is_Full(Item);

The procedure Pop also contains a lot of information.
The parameter Item is an instance of Stack which will be modified during the execution of the procedure Pop.
The parameter Value is an instance of Element_Type which will be set during the execution of the procedure Pop.
The pre-condition for procedure Pop is that the instance of Stack passed to the procedure cannot be empty.
The post-condition for procedure Pop is that the instance of Stack passed to the procedure will not be full upon completion of the procedure Pop.

The function Top

The function Top returns the value of the stack top stack element without changing the stack itself.
Example 9 The function Top
   function Top(Item : in Stack) return Element_Type with

     Pre => not Is_Empty(Item);

The parameter Item is an instance of the type Stack which is not modified during execution of the function Top.
The pre-condition for the function Top is that the instance of Stack passed to the function cannot be empty.

The procedure Clear

The procedure clear empties a stack.

   procedure Clear(Item : in out Stack) with

     Post => Is_Empty(Item);

Clear modifies the instance of Stack passed to it.
The post-condition specifies that the instance of Stack will be empty upon completion of the procedure Clear.

The file Bounded_Stack.adb

The file Bounded_Stack.adb contains the implementation of all the functions and procedures declared in the package specification.
All the code within the package body contained in the file Bounded_Stack.adb has visibility to all the code in the file Bounded_Stack.ads.
package body Bounded_Stack is


   --------------

   -- Is_Empty --

   --------------


   function Is_Empty (Item : Stack) return Boolean is

   begin

      return Item.Count = 0;

   end Is_Empty;


   -------------

   -- Is_Full --

   -------------


   function Is_Full (Item : Stack) return Boolean is

   begin

      return Item.Count = Item.Size;

   end Is_Full;


   ----------

   -- Push --

   ----------


   procedure Push(Item : in out Stack; Value : in Element_Type) is

   begin

      Item.Buf(Item.Index) := Value;

      Item.Index := Item.Index + 1;

      Item.Count := Item.Count + 1;

   end Push;


   ---------

   -- Pop --

   ---------


   procedure Pop (Item : in out Stack; Value : out Element_Type) is

   begin

      Value := Item.Top;

      Item.Index := Item.Index - 1;

      Item.Count := Item.Count - 1;

   end Pop;


   ---------

   -- Top --

   ---------


   function Top (Item : in Stack) return Element_Type is

   begin

      return Item.Buf(Item.Index - 1);

   end Top;


   -----------

   -- Clear --

   -----------


   procedure Clear(Item : in out Stack) is

   begin

      Item.Count := 0;

      Item.Index := 1;

   end Clear;


end Bounded_Stack;

You may notice that the implementations of the functions and procedures in Bounded_Stack.adb are very simple. There is no explicit check for empty or full stack conditions. The pre-conditions specified in each procedure or function specification are implicitly checked by code generated by the compiler. The post-conditions specified for each procedure or function are implicitly checked by code generated by the compiler. Failure of any specified pre-condition or post-condition results in an exception which, if unhandled,  terminates the program.

The file main.adb

with Bounded_Stack;

with Ada.Text_IO; use Ada.Text_IO;

with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;


procedure Main is

   package Int_Stack is new Bounded_Stack(Element_Type  => Integer,

                                          Default_Value => 0);

   use Int_Stack;

   My_Stack : Stack(Size => 10);

   Value : Integer;

begin

   while not My_Stack.Is_Full loop

      Put("Enter an integer: ");

      Get(Value);

      My_Stack.Push(Value);

   end loop;

   Put_Line("Printing and popping the stack:");

   while not My_Stack.Is_Empty loop

      My_Stack.Pop(Value);

      Put_Line(Integer'Image(Value));

   end loop;

   My_Stack.Pop(Value);

   Put_Line(Integer'Image(Value));

end Main;

The output of this program is:

Enter an integer: 1
Enter an integer: 2
Enter an integer: 3
Enter an integer: 4
Enter an integer: 5
Enter an integer: 6
Enter an integer: 7
Enter an integer: 8
Enter an integer: 9
Enter an integer: 0
Printing and popping the stack:
 0
 9
 8
 7
 6
 5
 4
 3
 2
 1

raised SYSTEM.ASSERTIONS.ASSERT_FAILURE : failed precondition from bounded_stack.ads:12 instantiated at main.adb:6
The main procedure pops all the values off the stack then attempts to pop one more value off the stack. Rather than corrupting the stack pointer the program raises the exception SYSTEM.ASSERTIONS.ASSERT_FAILURE. The exception message points to the pre-condition for the pop procedure requiring the stack to be not empty.

  • 29 August 2018 at 14:16

Incompetent C Programming Example

I have been bothered by the C coding samples I commonly find on the Web. Almost all of them contain grievous defects. The following example of an array-based stack using C is based upon C code published at http://www-cs.ccny.cuny.edu/~peter/dstest.html

The C code published at http://www-cs.ccny.cuny.edu/~peter/dstest.html supports the book Advanced Data Structures authored by Peter Braß and published by Cambridge University Press.

Array Stack

The C code for Array Stack is shown below.

Example 1 ArrayStack.c
#include <stdio.h>

#include <stdlib.h>


typedef int item_t;


typedef struct {item_t *base; item_t *top; int size;} stack_t;


stack_t *create_stack(int size)

{   stack_t *st;

    st = (stack_t *) malloc( sizeof(stack_t) );

    st->base = (item_t *) malloc( size * sizeof(item_t) );

    st->size = size;

    st->top = st->base;

    return( st );

}


int stack_empty(stack_t *st)

{   return( st->base == st->top );

}


int push( item_t x, stack_t *st)

{   if ( st->top < st->base + st->size )

    {   *(st->top) = x; st->top += 1;  return( 0 );

    }

    else

       return( -1 );

}


item_t pop(stack_t *st)

{   st->top -= 1;

    return( *(st->top) );

}


item_t top_element(stack_t *st)

{   return( *(st->top -1) );

}


void remove_stack(stack_t *st)

{   free( st->base );

    free( st );

}

int main()

{  stack_t *st;

   char nextop;

   st = create_stack(50);

   printf("Made Array-Based Stack of size 50\n");

   while( (nextop = getchar())!= 'q' )

   { if( nextop == 'i' )

     { int insitem;

       scanf(" %d", &insitem);

       push( insitem, st );

       printf(" pushed %d. The current top item is %d\n", insitem,

           top_element(st) );

     } 

     if( nextop == 'd' )

     { int de_item;

       getchar();

       de_item = pop(st);

       printf("  popped item %d", de_item);

       if( stack_empty(st) )

         printf(" the stack is now empty\n");

       else

         printf(" the top element is now %d\n",  top_element(st) );


     }

     if( nextop == '?' )

     { getchar();

       if( stack_empty(st) )

         printf("the stack is empty\n");

       else

         printf("the top element is %d\n", top_element(st) );

     }

    

   }

   remove_stack(st);

   printf(" removed stack\n");

   return(0);

}

The author claims that his book is not a text on Object Oriented Programming, but instead a text on advanced data structures.  None of the examples in the URL referenced above employ C header files. I assume that was done to simplify publication, placing both the data structure, including its associated functions, and a main program to demonstrate the structure, in a single file.

Issues with ArrayStack.c

While the author could have used array indexing to access the values of the array at the heart of the stack_t struct, he chose to perform pointer arithmetic directly. From a style point of view it is often preferred to use array indexing to access the elements of an array.

The push function

Example 2 push function
int push( item_t x, stack_t *st)

{   if ( st->top < st->base + st->size )

    {   *(st->top) = x; st->top += 1;  return( 0 );

    }

    else

       return( -1 );

}

Note that this code only actually pushes a value onto the stack if the stack is not full. This is a correct behavior. When the array is full this function returns -1.
Example 3 Calling push
   while( (nextop = getchar())!= 'q' )

   { if( nextop == 'i' )

     { int insitem;

       scanf(" %d", &insitem);

       push( insitem, st );

       printf(" pushed %d. The current top item is %d\n", insitem,

           top_element(st) );

     } 


The example  of calling the push function ignores the return value. This means that the push operation fails silently. The user is given no indication that the push fails. In fact, the printf statement following the push function call tells the user that the push succeeds, whether it does nor not.

The pop function

Example 4 The pop function
item_t pop(stack_t *st)

{   st->top -= 1;

    return( *(st->top) );

}

Please note that the pop function does not check for an empty stack. Logically, it is erroneous to pop a value from an empty stack. This function simply decrements the pointer st->top and returns whatever value is at that memory location. Since no check is made for an empty stack, the result can be to return the value at some memory location before the first element of the dynamically allocated array pointed to by st->base. This is a form of buffer overflow.
The main function calls the pop function before checking if the stack is empty. Checking the state of the stack before calling pop would be the correct behavior. Nothing in the main function prevents pop from being called when the stack is empty.
     if( nextop == 'd' )

     { int de_item;

       getchar();

       de_item = pop(st);

       printf("  popped item %d", de_item);

       if( stack_empty(st) )

         printf(" the stack is now empty\n");

       else

         printf(" the top element is now %d\n",  top_element(st) );


     }

If the pop function is called several times when the stack is empty the st->top pointer will continue to be decremented. If, after that the push function is called, the pushed value will be assigned to a memory location before the first element of the array, corrupting memory outside of the array.

The top_element function

A somewhat milder version of the problem with the pop function is present in the top_element function. This function returns the value of in the address one less than the current st-top pointer.
Example 5 The top_element function
item_t top_element(stack_t *st)

{   return( *(st->top -1) );

}

As you can see, the top_element function does not check for an empty stack.

The stack_empty function

Example 6 The stack_empty function
int stack_empty(stack_t *st)

{   return( st->base == st->top );

}

Note that this function will only return True when st->top points to the first element of the allocated array. It will return False when st->top points to any memory location before the beginning of the array. Since the function pop can move the pointer st->top to memory locations before the beginning of the array this function is highly unreliable.

Conclusions concerning ArrayStack.c

While this code may convey the general approach to implementing an array-based stack data structure, the actual implementation is faulty in many ways. All of the faults are associated with the primitive implementation of arrays in the C language and what seems to be a cultural tendency for C programmers to avoid dealing with error conditions. C arrays are simply a block of memory accessed by pointers. C provides no array bounds checking. Failing to specify error conditions is inexcusable, as is failing to evaluate return values of functions that do check for error conditions.
The code for this implementation may execute very efficiently, but it does no good when the code is executing erroneously.

Bounded_Stack

The Ada code for a bounded stack type is shown below. The following example implements a generic stack using an array with the size of the array determined at compile time.
Ada requires a package to implement both a specification, which is analogous to a C header file, and a body, which contains the implementation of the procedures and functions declared in the specification.

The file bounded_stack.ads

The file bounded_stack.ads contains the interface specification for a generic stack based upon an array.
generic

   type Element_Type is private;

   Default_Value : Element_Type;

package Bounded_Stack is

   type Stack(Size : Positive) is tagged private;

   function Is_Empty(Item : Stack) return Boolean;

   function Is_Full(Item : Stack) return Boolean;

   procedure Push(Item : in out Stack; Value : in Element_Type) with

     Pre => not Is_Full(Item),

     Post => not Is_Empty(Item);

   procedure Pop(Item : in out Stack; Value : out Element_Type) with

     Pre => not Is_Empty(Item),

     Post => not Is_Full(Item);

   function Top(Item : in Stack) return Element_Type with

     Pre => not Is_Empty(Item);

   procedure Clear(Item : in out Stack) with

     Post => Is_Empty(Item);

private

   type Buffer is array(Positive range <>) of Element_Type;

   type Stack(Size : Positive) is tagged record

      Buf   : Buffer(1..Size) := (Others => Default_Value);

      Index : Positive := 1;

      Count : Natural  := 0;

   end record;

end Bounded_Stack;

This is a generic package, as indicated by the reserved word “generic” at the start of the file. This package has two generic parameters; Element_Type, which designates any non-limited type, and Default_Value, which is used to initialize every instance of the stack with a properly define default value.
Declaring the type Stack to be private means that the details of the type are hidden from any calling subprogram or task. The subprogram declarations following the type declaration, and preceding the reserved word “private” are the only means given to manipulate the stack.

The procedure Push

Example 7 The procedure Push
   procedure Push(Item : in out Stack; Value : in Element_Type) with

     Pre => not Is_Full(Item),

     Post => not Is_Empty(Item);

The declaration of the procedure Push contains a lot of information.
The parameter Item must be an instance of the type Stack. The procedure Item will be modified during the execution of the procedure Push. The parameter Value is an instance of Element_Type, and it will not be modified during execution of the procedure Push.
The procedure Push has a simple pre-condition, namely that the instance of Stack passed to this procedure cannot be full before calling Push.
The procedure Push has a simple post-condition, namely that the instance of Stack passed to the procedure Push will not be empty upon completion of the procedure Push.
The pre-condition and the post-condition are enforced by the compiler.

The procedure Pop

Example 8 The procedure Pop
   procedure Pop(Item : in out Stack; Value : out Element_Type) with

     Pre => not Is_Empty(Item),

     Post => not Is_Full(Item);

The procedure Pop also contains a lot of information.
The parameter Item is an instance of Stack which will be modified during the execution of the procedure Pop.
The parameter Value is an instance of Element_Type which will be set during the execution of the procedure Pop.
The pre-condition for procedure Pop is that the instance of Stack passed to the procedure cannot be empty.
The post-condition for procedure Pop is that the instance of Stack passed to the procedure will not be full upon completion of the procedure Pop.

The function Top

The function Top returns the value of the stack top stack element without changing the stack itself.
Example 9 The function Top
   function Top(Item : in Stack) return Element_Type with

     Pre => not Is_Empty(Item);

The parameter Item is an instance of the type Stack which is not modified during execution of the function Top.
The pre-condition for the function Top is that the instance of Stack passed to the function cannot be empty.

The procedure Clear

The procedure clear empties a stack.

   procedure Clear(Item : in out Stack) with

     Post => Is_Empty(Item);

Clear modifies the instance of Stack passed to it.
The post-condition specifies that the instance of Stack will be empty upon completion of the procedure Clear.

The file Bounded_Stack.adb

The file Bounded_Stack.adb contains the implementation of all the functions and procedures declared in the package specification.
All the code within the package body contained in the file Bounded_Stack.adb has visibility to all the code in the file Bounded_Stack.ads.
package body Bounded_Stack is


   --------------

   -- Is_Empty --

   --------------


   function Is_Empty (Item : Stack) return Boolean is

   begin

      return Item.Count = 0;

   end Is_Empty;


   -------------

   -- Is_Full --

   -------------


   function Is_Full (Item : Stack) return Boolean is

   begin

      return Item.Count = Item.Size;

   end Is_Full;


   ----------

   -- Push --

   ----------


   procedure Push(Item : in out Stack; Value : in Element_Type) is

   begin

      Item.Buf(Item.Index) := Value;

      Item.Index := Item.Index + 1;

      Item.Count := Item.Count + 1;

   end Push;


   ---------

   -- Pop --

   ---------


   procedure Pop (Item : in out Stack; Value : out Element_Type) is

   begin

      Value := Item.Top;

      Item.Index := Item.Index - 1;

      Item.Count := Item.Count - 1;

   end Pop;


   ---------

   -- Top --

   ---------


   function Top (Item : in Stack) return Element_Type is

   begin

      return Item.Buf(Item.Index - 1);

   end Top;


   -----------

   -- Clear --

   -----------


   procedure Clear(Item : in out Stack) is

   begin

      Item.Count := 0;

      Item.Index := 1;

   end Clear;


end Bounded_Stack;

You may notice that the implementations of the functions and procedures in Bounded_Stack.adb are very simple. There is no explicit check for empty or full stack conditions. The pre-conditions specified in each procedure or function specification are implicitly checked by code generated by the compiler. The post-conditions specified for each procedure or function are implicitly checked by code generated by the compiler. Failure of any specified pre-condition or post-condition results in an exception which.if unhandled,  terminates the program.

The file main.adb

with Bounded_Stack;

with Ada.Text_IO; use Ada.Text_IO;

with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;


procedure Main is

   package Int_Stack is new Bounded_Stack(Element_Type  => Integer,

                                          Default_Value => 0);

   use Int_Stack;

   My_Stack : Stack(Size => 10);

   Value : Integer;

begin

   while not My_Stack.Is_Full loop

      Put("Enter an integer: ");

      Get(Value);

      My_Stack.Push(Value);

   end loop;

   Put_Line("Printing and popping the stack:");

   while not My_Stack.Is_Empty loop

      My_Stack.Pop(Value);

      Put_Line(Integer'Image(Value));

   end loop;

   My_Stack.Pop(Value);

   Put_Line(Integer'Image(Value));

end Main;

The output of this program is:

Enter an integer: 1
Enter an integer: 2
Enter an integer: 3
Enter an integer: 4
Enter an integer: 5
Enter an integer: 6
Enter an integer: 7
Enter an integer: 8
Enter an integer: 9
Enter an integer: 0
Printing and popping the stack:
 0
 9
 8
 7
 6
 5
 4
 3
 2
 1

raised SYSTEM.ASSERTIONS.ASSERT_FAILURE : failed precondition from bounded_stack.ads:12 instantiated at main.adb:6
The main procedure pops all the values off the stack then attempts to pop one more value off the stack. Rather than corrupting the stack pointer the program raises the exception SYSTEM.ASSERTIONS.ASSERT_FAILURE. The exception message points to the pre-condition for the pop procedure requiring the stack to be not empty.

  • 11 June 2018 at 23:03

Comparing Ada and High Integrity C++

I have often suspected that use of a safety critical or high integrity coding standard for C++ would yield a level of safety and software reliability approximately equivalent to using Ada with no restrictions.
I have documented a comparison of the High Integrity C++ Coding Standard (HIC) produced by PRQA with standard Ada language features. I was mostly correct in my suspicions. There are some rules in the HIC which apply equally well to Ada, such as a prohibition against the use of the goto statement.
In many instances the HIC rules require a non-trivial amount of code development and verification, while the Ada solution is trivial. For instance, achieving object initialization in C++ requires the use of carefully implemented constructors, while specifying default initialization for Ada records is relatively trivial. Another example is C++ multi-threading. The HIC lists several rules for the use of locks, mutexes, and condition variables. For Ada, the built-in facilities of task Rendezvous for direct task communication, and protected objects for communication through shared buffers, includes implicit control of locks, mutexes, and condition variables.

A PDF version of this document is available at https://drive.google.com/open?id=0B0y7MZFreWQtRy0wR0g3NTF6XzQ

My comparison follows:

_________________________________________________________________________

Comparison of Ada 2012 with the PRQA High Integrity C++ Coding Standard
This document compares the safety critical subset of C++ defined in the High Integrity C++ Coding Standard Version 4.0 (HIC) produced by PRQA and available at www.codingstandard.com.
The comparison will follow the flow of the HIC.

1.     General

1.1.  Implementation Compliance

1.1.1.     Ensure that code complies with the 2011 ISO C++ Language Standard

Compilers often provide features beyond those defined in the Standard, and unrestricted usage of such features will likely hamper code portability. To this end code should be routinely parsed with a separate compiler or code analysis tool apart from the compiler used for production purposes.

Corresponding Ada rule:

Compilers often provide features beyond those defined in the Standard, and unrestricted usage of such features will likely hamper code portability. To this end Ada compilers provide the Ada profile No_Implementation_Extensions, which enforces the restriction to Standard Ada in a standard way. The only thing not caught by this profile is the use of implementation-defined library packages, such as the GNAT packages for the GNAT compiler. Those packages can be caught by adding the No_Dependence profile.

1.2.  Redundancy

1.2.1.     Ensure that all statements are reachable

For the purposes of this rule, missing else and default clauses are considered also.
If a statement cannot be reached for any combination of function inputs (e.g. function arguments, global variables, volatile objects), it can be eliminated.

Corresponding Ada rule:

While Ada contains the same rule, Ada compilers perform reachability analysis and report compilation errors when code is not reachable.
Ada compilers identify missing others clauses in case statements when all possible values are not explicitly included in a case statement.

1.2.2.     Ensure that no expression or sub-expression is redundant

An expression statement with no side effects can be removed or replaced with a null statement without effecting behavior of the program.
Similarly, it is sometimes possible to simplify an expression by removing operands that do not change the resulting value of the expression, for example by multiplying by 1 or 0.
Redundant code causes unnecessary maintenance overhead and may be a symptom of a poor design.

Corresponding Ada rule:

The corresponding Ada rule would be identical. This rule applies to any programming language.

1.3.  Deprecated Features

1.3.1.     Do not use the increment operator (++) on a variable of type bool

Incrementing an object of type bool results in its value being set to true. This feature was deprecated in the 1998 C++ Language Standard and thus may be withdrawn in a later version.
Prefer to use an explicit assignment to true.

Corresponding Ada rule:

The Ada Boolean type is an enumerated type with the values of (False, True). Ada does not allow arithmetic operations on enumerated types. Furthermore, Ada does not provide an increment operator.

1.3.2.     Do not use the register keyword

Most compilers ignore the register keyword, and perform their own register assignments. Moreover, this feature was deprecated in the 2011 Language Standard and thus may be removed in a later version.

Corresponding Ada rule:

Ada does not provide any equivalent to the register keyword in C++.

1.3.3.     Do not use the C Standard Library .h headers

The C standard library headers are included in C++ for compatibility. However, their inclusion was deprecated in the 1998 C++ Language Standard and thus may be withdrawn in a later version.

Corresponding Ada rule:

Ada does not provide a preprocessor and does not interface with C or C++ programs at the source code level.

1.3.4.     Do not use deprecated STL features

The following STL features were deprecated in the 2011 C++ Language Standard and thus may be withdrawn in a later version:
·         Std::auto_ptr
·         Std::bind1st
·         Std::bind2nd
·         Std::ptr_mem_fun_ref
·         Std::unary_function
·         Std::binary_function
Of particular note is std::auto_ptr as it has been suggested that a search and replace of this type to std::unique_ptr may uncover latent bugs to the incorrect use of std::auto_ptr.

Corresponding Ada rule:

Avoid the use of the language features listed in Appendix J of the Ada 2012 Language Reference Manual. Use Ada 2012 aspect specifications instead. Avoiding Annex J features can be enforced through the use of the profile No_Obsolescent_Features.

1.3.5.     Do not use throw exception specifications

Specifying an exception specification using throw( type-id-listopt ) has been deprecated in the 2011 C++ Language Standard and thus may be removed in a future version.
The new syntax is to use noexcept or noexcept( expr).

Corresponding Ada rule:

Ada does not have an equivalent to either noexcept or noexcept(expr). In C++ the noexcept keyword verifies that no exception is thrown in a specified function. The noexcept(expr) declares that a function may throw exceptions for some types but not other types. Both forms of noexcept return a Boolean result.
The SPARK subset of Ada prohibits exceptions and is able to prove that a subprogram will not raise an exception. Exceptions are prohibited because of the difficulty in formally proving the correctness of a program which raises exceptions.

2.     Lexical Conventions

2.1.  Character sets

2.1.1.     Do not use tab characters in source files

Tab width is not consistent across all editors or tools. Code indentation can be especially confusing when tabs and spaces are used interchangeably. This may easily happen where code is maintained in different editors.
In string and character literals \t should be used in preference to a direct tab character.

Corresponding Ada rule:

Use of tabs in Ada source code is subject to the same editor and tool variations as use of a tab in C++ source code.
Insertion of a tab character in a string or character literal or variable should be use Ada.Characters.Latin_1.HT.

2.2.  Trigraph sequences

2.2.1.     Do not use digraphs or trigraphs

Trigraphs are special three character sequences, beginning with two question marks and followed by one other character. They are translated into specific single characters, e.g. \ or ^. Digraphs are special two character sequences that are similarly translated.

Corresponding Ada rule:

Ada does not provide digraph or trigraph sequences.

2.3.  Comments

2.3.1.     Do not use the C comment delimiters /* … */

The scope of C++ comments is clearer; until the end of a logical source line (taking line splicing into account). Errors can result from the use of C comments.

Corresponding Ada rule:

Ada does not allow the use of C comments.

2.3.2.     Do not comment out code

Source code should always be under version control. Therefore keeping old code in comments is unnecessary. It can make browsing,  searching, and refactoring the source code more difficult.

Corresponding Ada rule:

The same rule should apply to any programming language.

2.4.  Identifiers

2.4.1.     Ensure that each identifier is distinct from any other visible identifier

Similarity of identifiers impairs readability, can cause confusion and may lead to mistakes.
Names should not differ only in case (foo/Foo) or in the use of underscores(foobar/foo_bar). Additionally, certain combinations of characters look very similar.
Note: This rule does not require that an identifier cannot be reused.

Corresponding Ada rule:

Ada identifiers are case-insensitive, thus (foo/Foo) are treated as the same identifier.
Ada compilers identify duplicate definition of identifier within the same scope.

2.5.  Literals

2.5.1.     Do not concatenate strings with different encoding prefixes

The C++ Standard permits string literals with the following encoding prefixes: u, U, u8, L. A program that concatenates a pair of string literals with u8 and L prefixes is ill-formed.
The result of the remaining prefix combinations are all implementation defined. For this reason encoding sequences should not be mixed.

Corresponding Ada rule:

Ada provides multiple character encoding sequences. Each sequence is a distinct type and therefore each string type is unique. Ada does not allow mixing data types within a single array.

2.5.2.     Do not use octal constants (other than zero)

Octal constants are specified with a leading digit 0; therefore, literal 0 is technically an octal constant.
Do not use any other octal literals, as based on unfamiliarity, this could be confusing and error prone.

Corresponding Ada rule:

Ada allows the use of any number base from 2 through 16. A literal of a particular number base is expressed as base#digits#  (e.g 2#111# is the binary equivalent of 7 base 10) except for base 10, which is implied by simply expressing the digits. Therefore, 11 and 011 are interpreted as 11 base 10.

2.5.3.     Use nullptr for the null pointer constant

The 2011 C++ Language Standard introduced the nullptr keyword do denote a null pointer constant.
The NULL macro and constant expression 0 can be used in both pointer contexts and integral contexts. nullptr, however, is only valid for use in pointer contexts and so cannot be unexpectedly used as an integral value.

Corresponding Ada rule:

Ada access types are functionally equivalent to pointers but are not pointers. Ada access types can be set to null, which is not a numerical value and cannot be confused as an integral value. The address of a variable can be set using with address aspect of a variable declaration:
Foo : Integer with Address => 16#3ff#;

3.     Basic Concepts

3.1.  Scope

3.1.1.     Do not hide declarations

Reusing the same identifier for different declarations is confusing and difficult to maintain. If the hiding declaration is later removed, or the identifier is renamed, a compilation error may not be generated, as the declaration that was previously hidden will now be found.
While hidden namespace scope identifiers can still be accessed with a fully qualified name, hidden block scope identifiers will not be accessible.
In C++ it is possible for the same identifier to refer to both a type and an object or a function. In this case the object or function will hide the type.

Corresponding Ada rule:

Use of an identifier in a local scope that matches a visible identifier in a dependent package will result in a compiler error. Use of an identifier in an inner block will hide the use of the same identifier an enclosing block.
Ada does not allow an identifier to refer to both a type and an object or a function within the same or enclosing scopes.

3.2.  Program and linkage

3.2.1.     Do not declare functions at block scope

A declaration for a function should not be common to its definition, any redeclarations, and any calls to it.
To ensure that the same type is used in all declarations, functions should always be declared at namespace scope(See Rules 7.4.3: “Ensure that an object or a function used from multiple translation units is declared in a single header file” and 7.4.1: “Ensure that any objects, functions, or types to be used from a single translation unit are defined in an unnamed namespace in the main source file”).

Corresponding Ada rule:

Ada provides the package construct to enforce name space and encapsulation. Any type, object, subprogram, or task used by more than one compilation unit must be declared within a package. Ada provides no other means to share declarations between compilation units.
Ada does allow a single subprogram or task to define types, objects, subprograms or tasks to be used only within the defining subprogram or task.

3.3.  Storage duration

3.3.1.     Do not use variables with static storage duration

Variables with linkage (and hence static storage duration), commonly referred to as global variables, can be accessed and modified from anywhere in the translation unit if they have internal linkage, and anywhere in the program if they have external linkage. This can lead to uncontrollable relationships between functions and modules.
Additionally, certain aspects of the order of initialization of global variables are unspecified and implementation defined in the C++ Language Standard. This can lead to unpredictable results for global variables that are initialized at run-time (dynamic initialization).
This rule does not prohibit the use of a const object with linkage, so long as:
·         It is initialized through static initialization
·         The object is no ODR used
The order of initialization of block scope objects with static storage is well defined. However, the lifetime of such an object ends at program termination, which may be incompatible with future uses of the code, e.g. as a shared library. It is preferable to use objects with dynamic storage duration to represent program state, allocated from the heap or a memory pool.

Corresponding Ada rule:

The only way to share variables across compilation units in Ada is by declaring those variables within a package. The order of initialization of constants is controlled by package elaboration rules. While Ada does provide default elaboration rules, it also provides elaboration pragmas allowing the programmer to specify elaboration order for packages.

3.4.  Object lifetime

3.4.1.     Do not return a reference to a pointer or an automatic variable defined with the function

The lifetime of a variable with automatic storage duration ends on exiting the enclosing block. If a reference or a pointer to such a variable is returned from a function, the lifetime of the variable will have ended before the caller can access it through the returned handle, resulting in undefined behavior.

Corresponding Ada rule:

When an object is destroyed in Ada, whether the object was created through allocation from a storage pool, or on the program stack, the Finalize procedure for that object is called. There are no exceptions in Ada.
Ada accessibility rules, which prevent dangling references, are written in terms of accessibility levels, which reflect the run-time nesting of masters. As explained in 7.6.1, a master is the execution of a certain construct, such as a subprogram_body. An accessibility level is deeper than another if it is more deeply nested at run time. For example, an object declared local to a called subprogram has a deeper accessibility level than an object declared local to the calling subprogram. The accessibility rules for access types require that the accessibility level of an object designated by an access value be no deeper than that of the access type. This ensures that the object will live at least as long as the access type, which in turn ensures that the access value cannot later designate an object that no longer exists. The Unchecked_Access attribute may be used to circumvent the accessibility rules.
A given accessibility level is said to be statically deeper than another if the given level is known at compile time to be deeper than the other for all possible executions. In most cases, accessibility is enforced at compile time by Legality Rules. Run-time accessibility checks are also used, since the Legality Rules do not cover certain cases involving access parameters and generic packages.

3.4.2.     Do not assign the address of a variable to a pointer with a greater lifetime

The C++ Standard defines 4 kinds of storage duration:
·         Static
·         Thread
·         Automatic
·         Dynamic
The lifetime of objects with the first 3 kinds of storage duration is fixed, respectively:
·         Until program termination
·         Until thread termination
·         Upon exiting the enclosing block
Therefore, undefined behavior will likely occur if an address of a variable with automatic storage duration is assigned to a pointer with static storage duration, or one defined in an outer block. Similarly, for a thread_local  variable aliased to a pointer with static storage duration.
If using high_integrity::thread, then references or pointers with local storage duration should not be passed into threads that have the high_integrity::DETACH property.

Corresponding Ada rule:

Ada accessibility rules, which prevent dangling references, are written in terms of accessibility levels, which reflect the run-time nesting of masters. As explained in 7.6.1, a master is the execution of a certain construct, such as a subprogram_body. An accessibility level is deeper than another if it is more deeply nested at run time. For example, an object declared local to a called subprogram has a deeper accessibility level than an object declared local to the calling subprogram. The accessibility rules for access types require that the accessibility level of an object designated by an access value be no deeper than that of the access type. This ensures that the object will live at least as long as the access type, which in turn ensures that the access value cannot later designate an object that no longer exists. The Unchecked_Access attribute may be used to circumvent the accessibility rules.
A given accessibility level is said to be statically deeper than another if the given level is known at compile time to be deeper than the other for all possible executions. In most cases, accessibility is enforced at compile time by Legality Rules. Run-time accessibility checks are also used, since the Legality Rules do not cover certain cases involving access parameters and generic packages.

3.4.3.     Use RAII for all resources

Objects with non-trivial destructors and automatic storage duration have their destructors called implicitly when they go out of scope. The destructor will be called for both normal control flow and when an exception is thrown.
The same principle does not apply for a raw handle to a resource, e.g. a pointer to allocated memory. By using a manager class, the lifetime of the resource can be correctly controlled, specifically by releasing it in the destructor.
This idiom is known as Resource Allocation Is Initialization (RAII) and the C++ Language Standard provides RAII wrappers for many resources, such as:
·         Dynamically allocated memory, e.g. std::unique.ptr
·         Files, e.g. std::ifstream
·         Mutexes, e.g. std::lock_guard

Corresponding Ada rule:

The Ada language provides the package Ada.Finalization, which defines two data types:
·         Controlled  – which provides the procedures:
o   Initialize
o   Adjust
o   Finalize
·         Limited_Controlled  - which provides the following procedures
o   Initialize
o   Finalize

3.5.  Types

3.5.1.     Do not make any assumptions about the internal representation of a value or an object

Avoid C++ constructs and practices that are likely to make your code non-portable:
·         A union provides a way to alter the type ascribed to a value without changing its representation. This reduces type safety and is usually unnecessary. In general it is possible to create a safe abstraction using polymorphic types.
·         Integer types other than signed / unsigned char have implementation defined size. Do not use integer types directly, instead use size specified typedefs, defined in a common header file, which can be easily adjusted to match a particular platform.
·         Do not mix bitwise arithmetic operations on the same variable, as this it likely to be non-portable between big and little endian architectures.
·         Do not assume the layout of objects in memory, e.g. by comparing pointers to different objects with relational operators, using the offsetof macro, or performing pointer arithmetic with unspecified or implementation defined layout.

Corresponding Ada rule:

Ada also has predefined types. In the case of the predefined integer and floating point types it is advised that the programmer define types or subtypes which better meet their needs. Ada allows the programmer to specify the valid range of values for numeric types, along with specific memory characteristics.
Ada allows the programmer to define the size of an object or a subtype in bits. The size may of an integer, for instance, may not exceed the size of the largest integer representation allowed on the target hardware. The size of a component of a compound type may be specified, and the specific memory layout of record components may be specified. Record components can be less than a word in size, or their size may extend across multiple words.
The size of an object can be interrogate at run time. For example, a packed array of Boolean values will represent each array element in one bit. Each bit can be individually indexed through the standard Ada array indexing notation.

4.     Standard Conversions

4.1.  Array to pointer conversion

When an array is bound to a function parameter of pointer type the array is implicitly converted to a pointer to the first element of the array.
In order to retain the array dimension, the parameter should be changed to a reference type or changed to a user defined type such as std::array.

Corresponding Ada rule:

Ada does not provide array to pointer conversion. An array is a first class type. The dimension and index values of the array are available wherever the array object is visible.

4.2.  Integral conversions

4.2.1.     Ensure that the U suffix is applied to a literal in a context requiring an unsigned integral expression

If a literal is used to initialize a variable of unsigned type, or with an operand of unsigned type in a binary operation, the U suffix should be appended to the literal to circumvent an implicit conversion, and make the intent explicit.

Corresponding Ada rule:

Ada does not provide implicit conversions between data types.  An unsigned type in Ada is known as a modular type. All integer literals in Ada are of the type Universal Integer. A value of type Universal Integer will be implicitly converted to the target type during assignment or in an arithmetic operation. Ada will raise a compile-time error if the literal is not within the range of the target type during an assignment. Ada will raise a run time exception if the result of the arithmetic operation is not within the range of the target type.
The programmer is allowed to define unique numeric data types. There are no implicit conversions between programmer defined data types, even when they two types share the same memory layout. Ada is strongly typed.

4.2.2.     Ensure that data loss does not demonstrably occur in integral expressions

Data loss can occur in a number of contexts:
·         Implicit conversions
·         Type casts
·         Shift operations
·         Overflow in signed arithmetic operations
·         Wraparound in unsigned arithmetic operations
If possible, integral type conversions should be avoided altogether, by performing operations in a uniform type matched to the execution environment.
Where data storage is a concern, type conversions should be localized with appropriate guards (e.g. assertions) to detect data loss.
Similar techniques can be used to guard shift and arithmetic operations, especially where the data is tainted in a security sense, i.e.  a malicious user can trigger data loss with appropriately crafted input data.
Data loss may also occur if high order bits are lost in a left shift operation, or the right hand operator of a shift operator is so large that the resulting value is always 0 or undefined regardless of the value of the left hand operand.
Therefore, appropriate safeguards should be coded explicitly (or instrumented by a tool) to ensure that data loss does not occur in shift operations.
For the purposes of this rule integral to bool conversions are considered to result in data loss as well. It is preferable to use equality or relational operators to replace such type conversions. The C++ Language Standard states that unless the condition of an if, for, while, or do statement had a Boolean type, it will implicitly converted to bool.
Note: An implicit conversion using an operator bool declared as explicit does not violate this rule.

Corresponding Ada rule:

All integral data operations in Ada are protected by range checks. Any violation of a range specification at run time will result in the exception Constraint_Error.
Ada only allows bit shifting operations on unsigned types.
The Ada Boolean type is not numeric, and therefore does not undergo conversion to an integral type. All conditions of an if, for, or while statement must result in a Boolean value.

4.3.  Floating point conversions

The C++ Standard provides 3 floating point types: float, double, long double that, at least conceptually, increase in precision.
Expressions that implicitly or explicitly cause a conversion from long double to float or double, and from double to float should be avoided as they may result in data loss.
When using a literal in a context that requires a type float, use the F suffix, and for consistency use the L suffix in a long double context.

Corresponding Ada rule:

Ada provides a predefined floating point type named float, which should provide at least 6 decimal digits of precision. The programmer is allowed to define custom floating point types with a user specified range and precision.
There is no implicit conversion between floating point types.
Ada also provides the ability to define fixed point types. Fixed point types are implemented as scaled integers. Fixed point types have a constant number of digits after the decimal place, while floating point types have a variable number of digits after the decimal place (or none at all) depending upon the magnitude of the value represented.
There is no implicit conversion between numeric types in Ada.

4.4.  Floating-integral conversions

4.4.1.     Do not convert floating values to integral values except through use of standard functions

An implicit or explicit conversion from a floating to an integral type can result in data loss due to the significant difference in the respective range of values for each type.
Additionally, floating point to integral type conversions are biased as the fractional part is simply truncated instead of being rounded to the nearest integral value. For this reason use one of the standard library functions: std::floor and std::ceil is recommended if a conversion to an integral type is necessary.
Note: A return value of std::floor and std::ceiling is of floating type, and an implicit or explicit conversion of this value to an integral type is permitted.

Corresponding Ada rule:

There are no implicit conversions between Ada numeric types. Every floating point type and subtype has the attributes S’Ceiling and S’Floor defined to calculate the Ceiling and Floor of the floating point value.
If an explicit conversion between a floating point value and an integral value is performed the conversion  will include a range check. If the resulting value is out of range of the integral type the exception Constraint_Error will be raised.

5.     Expressions

5.1. Primary expressions

5.1.1.     Use of symbolic names instead of literal values in code

Use of “magic” numbers and strings in expressions should be avoided in preference to constants with meaningful names.
The use of named constants improves both the readability and maintainability of the code.

Corresponding Ada rule:

Use of constants is recommended over the use of literals.

5.1.2.     Do not rely on the sequence of evaluation within an expression

To enable optimizations and parallelization, the C++ Standard uses a notation of sequenced before, e.g.:
·         Evaluation of a full expression is sequenced before the next full-expression to be evaluated
·         Evaluation operands of an operator are sequenced before evaluation of the operator
·         Evaluation of arguments in a function call are sequenced before the execution of the function
·         For built-in operators &&, ||,  and operator ? evaluation of the first operand is sequenced before evaluation of the other operand(s)
This defines a partial order on evaluations, and where two evaluations are unsequenced with respect to one another, their execution can overlap. Additionally, two evaluations may be indeterminately sequenced, which is similar, except that the execution cannot overlap.
This definition leaves great latitude to a compiler to re-order evaluation of sub-expressions, which can lead to unexpected, and even undefined behavior. For this reason, and to improve readability an expression should not:
·         Have more than one side effect
·         Result in the modification and access of the same scalar object
·         Include a sub-expression that is an assignment operation
·         Include a sub-expression that is a pre- or post-increment/decrement operation
·         Include a built-in comma operator (for overloaded comma operation see Rule 13.2.1: “Do not overload operators with special semantics”)

Corresponding Ada rule:

In Ada assignment is not an expression, and cannot therefore be part of a compound expression. Furthermore, there are no pre- or post-increment/decrement expressions in Ada. These limitations limit the problems associated with violation of Rule 5.1.2.
Ada 2012 does allow the use of “in out” parameters in functions, which can lead to order dependencies. Early versions of Ada prohibited “in out” parameters in functions, allowing only “in” parameters, to avoid this problem. It is still recommended that functions only use “in” parameters except when interfacing with other languages, such as C and C++.

5.1.3.     Use parentheses in expressions to specify the intent of the expression

The effects of precedence and associativity of operators in a complex expression can be far from obvious. To enhance readability and maintainability of code, no reliance on precedence or associativity should be made, by using explicit parentheses, except for:
·         Operands of assignment
·         Any combination of + and operations only
·         Sequence of && operations only
·         Sequence of ||operations only

Corresponding Ada rule:

In every language it is recommended to use explicit parentheses instead of relying upon precedence and associativity rules.
In Ada the logical and operator is and. The logical or operator is or. The equivalent of the C++ && is “and then” and the equivalent of the C++ || is “or else”.

5.1.4.     Do not capture variables implicitly in a lambda

Capturing variables helps document the intention of the author. It also allows for different variables to be captured by copy and captured by reference within the same lambda.
Exception:

It is not necessary to capture objects with static storage duration or constants that are not ODR used.

However, the use of objects with static storage duration should be avoided. See Rule 3.3.1: ”Do not use variables with static storage duration”.

Corresponding Ada rule:

Ada does not provide lamdas. It is possible to pass a subroutine (function or procedure) as a generic parameter.

5.1.5.     Include a (possibly empty) parameter list in every lambda

The lambda-declarator is optional in a lambda expression and results in a closure that can be called without any parameters.
To avoid any visual ambiguity with other C++ constructs, it is recommended to explicitly include ( ), even though it is not strictly required.

Corresponding Ada rule:

Ada does not provide lamdas. It is possible to pass a subroutine (function or procedure) as a generic parameter.

5.1.6.     Do not code side effects into the right-hand operands of: &&, ||, sizeof, typeid or a function passed to condition variable::wait

For some expressions, the side effect of a sub-expression may not be evaluated at all or can be conditionally evaluated, that is, evaluated only when certain conditions are met. For Example:
·         The right-hand operands of the && and || operators are only evaluated if the left hand operand is true and false respectively.
·         The operand of sizeof is never evaluated.
·         The operand of typeid is evaluated only if it is a function call that returns reference to a polymorphic type.
Having visible side effects that do not take place, or only take place under special circumstances makes the code harder to maintain and can also make it harder to achieve a high level of test coverage.
Conditional Variable: wait member

Every time a waiting thread wakes up, it checks the condition. The wake-up may not necessarily happen in direct response to a notification from another thread. This is called a spurious wake. It is indeterminate how many times and when such spurious wakes happen. Therefore it is advisable to avoid using a function with side effects to perform the condition check.

Corresponding Ada rule:

The same rule applies to the Ada “and then” and “or else” logical operations. The right hand side of the “and then” operation is only evaluated if the left hand side is True. The right hand side of the “or else” operation is only evaluated if the left hand side is False.

5.2.  Postfix expressions

5.2.1.     Ensure that pointer or array access is demonstrably within bounds of a valid object

Unlike standard library containers, arrays do not benefit from bounds checking.
Array access can take one of the equivalent forms: *(p + i) or p[i], and will result in undefined behavior, unless p and p + i point to elements of the same array object. Calculating (but not dereferencing) an address one past the last element of the array is well defined also. Note that a scalar object can be considered as equivalent to an array dimensioned to 1.
To avoid undefined behavior, appropriate safeguards should be coded explicitly (or instrumented by a tool), to ensure that array access is within bounds, and that indirection operations (*) will not result in a null pointer dereference.

Corresponding Ada rule:

Ada makes these checks automatically, so there is no need for manually making them or using some separate tool that also needs to be validated.  The compiler will statically check values for array bounds violations and issue a compilation error when a bounds violation is detected. The Ada compiler also automatically inserts bounds checking into array accesses as needed.
Moreover, the range of the index values for an array can be any contiguous range of discrete values. Ada array indexing is not tied to pointer arithmetic and the lowest bound for an array need not be 0. Ada array definitions include a definition of the array type and range as well as the element type. Arrays have well defined attributes available to the programmer wherever the array object is visible.
Table 1 Attributes of array A
Array Attribute
Description
A’Length
The number of elements in the array
A’First
The value of the lowest index value for the array
A’Last
The value of the highest index value for the array
A’Range
The range of index values for the array; equivalent to A’First..A’Last
Iterating through an array safely in Ada is done using the Ada for loop:
For loop syntax
Description
for I in A’Range loop

   … do something

end loop;

Iterate through the index values for the range of array A, doing something for each index value.
for value of A loop

  … do something

end loop;

Iterate through array A referencing each value of A in turn. This is equivalent to a “for each” loop in some other languages

5.2.2.     Ensure that functions do not call themselves, either directly or indirectly

As the program stack tends to be one of the more limited resources, excessive use of recursion may limit the scalability and portability of the program. Tail recursion can readily be replaced with a loop. Other forms of recursion can be replaced with an iterative algorithm and worklists.

Corresponding Ada rule:

If you want to enforce this rule in Ada you can use the profile No_Recursion.

5.3.  Unary expressions

5.3.1.     Do not apply unary minus to operands of unsigned type

The result of applying a unary minus operator (-) to an operand of unsigned type (after integral promotion) is a value that is unsigned and typically very large.
Prefer to use the bitwise complement (~) operator instead.

Corresponding Ada rule:

The rule here is the same for Ada. The result for an Ada unsigned type is well defined, but may not be what the programmer intends.

5.3.2.     Allocate memory using new and release it using delete

C style allocation is not type safe, and does not invoke constructors or destructors. For this reason only operators new and delete should be used to manage objects with dynamic storage duration.
Note: Invoking delete on a pointer allocated with malloc or invoking free on a pointer allocated with new will result in undefined behavior.

Corresponding Ada rule:

All Ada allocation / deallocation mechanisms are type safe.

5.3.3.     Ensure that the form of delete matches the form of new used to allocate the memory

The C++ Standard requires that the operand to the delete operator is either:
·         a null pointer
·         pointer to a non array object allocated with new
·          pointer to a base class3 subobject of a non array object allocated with new
Similarly, the operand to the delete[] operator is either:
·         a null pointer
·         pointer to an array object allocated with new[]
In order to avoid undefined behavior, plain and array forms of delete and new should not be mixed.

Corresponding Ada rule:

Ada does not have this problem.
There are no special forms of new for allocating arrays in Ada.  An Ada array is a first class type and the compiler knows the size of an array. The new operator allocates the memory needed to hold an object of the type specified.
Ada does requires the generic function Ada.Unchecked_Deallocation be instantiated for the specific type to be deallocated. The generic parameters for this function are the access type and the base type being deallocated.

5.4.  Explicit type conversion

5.4.1.     Only use casting forms: static_cast (excl, void*), dynamic_cast or explicit constructor call

All casts result in some degree of type punning, however, some casts may be considered more error prone than others:
·         It is undefined behavior for the result of a static cast to void* to be cast to any type other than the original from type.
·         Depending on the type of an object, casting away const or volatile and attempting to write to the result is undefined behavior.
·         Casts using reinterpret cast are generally unspecified and/or implementation defined. Use of this cast increases the effort required to reason about the code and reduces its portability.
·         Simplistically, a C-style cast and a non class function style cast can be considered as a sequence of the other cast kinds. Therefore, these casts suffer from the same set of problems. In addition, without a unique syntax, searching for such casts in code is extremely difficult.

Corresponding Ada rule:

Ada uses the term type conversion when converting a value from one type to another type. There is only one syntax for type conversion:  subtype-name (expression-or-identifier)
If the target type of a conversion is an integer type and the operand is a real type then the result is the result rounded to the nearest integer (away from zero if exactly half way between two integers). Range checking is done after the converted value has been calculated.

5.4.2.     Do not cast an expression to an enumeration type

The result of casting an integer to an enumeration type is unspecified if the value is not within the range of the enumeration. This also applies when casting between different enumeration types.
For this reason conversions to an enumeration type should be avoided.

Corresponding Ada rule:

Converting between an integer and an enumeration value is not performed by type conversion. Instead, enumeration types have the attributes T’Pos and T’Val. T’Pos takes an argument of the enumeration type and returns the position number of that argument. T’Val takes an integer as an argument and returns the value of type T whose position number equals the value of the argument. If there is no value with the given position number the exception Constraint_Error is raised.

5.4.3.      Do not convert from a base class to a derived class

The most common reason for casting down an inheritance hierarchy, is to call derived class methods on an object that is a reference or pointer to the base class.
Using a virtual function removes the need for the cast completely and improves the maintainability of the code.

Corresponding Ada rule:

In Ada casting between levels of an inheritance hierarchy is called a view conversion. Converting from a base type to a derived type is valid only when the derived type does not add any data members to the set of members defined in the base type.
In Ada polymorphic types are called tagged types.

5.5.  Multiplicative operators

5.5.1.     Ensure that the right hand operand of the division or remainder operations is non-zero

The result of integer division or remainder operation is undefined if the right hand operand is zero. Therefore, appropriate safeguards should be coded explicitly (or instrumented by a tool) to ensure that division by zero does not occur.

Corresponding Ada rule:

The result of division by zero is well defined in Ada. The result will cause the exception Constraint_Error to be raised.

5.6.  Shift operators

5.6.1.     Do not use bitwise operators with signed operands

Use of signed operands with bitwise operators is in some cases subject to undefined or implementation defined behavior. Therefore, bitwise operators should only be used with operands of unsigned integral types.

Corresponding Ada rule:

In Ada the bitwise operators are only defined for unsigned types.

5.7.  Equality operators

5.7.1.     Do not write code that expects floating point calculations to yield exact results

Floating point calculations suffer from machine precision (epsilon), such that the exact result may not be representable.
Epsilon is defined as the difference between 1 and the smallest value greater than 1 that can be represented in a given floating point type. Therefore, comparisons of floating point values need to take epsilon into account.

Corresponding Ada rule:

Floating point number exhibit the same behavior in Ada. If you want exact equality in a real number then use a fixed point representation instead of a floating point representation.

5.7.2.     Ensure that a pointer to a member that is a virtual function is only compared (==) with nullptr

The result of comparing a pointer to member to a virtual function to anything other than nullptr is unspecified.

Corresponding Ada rule:

Ada polymorphism does not depend upon pointers to members of a class hierarchy. Parameters to subprograms can be defined as class-wide parameters, and the actual parameter can be any object with the class hierarchy.

5.8.  Conditional operator

5.8.1.     Do not use the conditional operator (?:) as a sub-expression

Evaluation of a complex condition is best achieved through explicit conditional statements (if/else). Using the result of the conditional operator as an operand reduces the maintainability of the code.
The only permissible uses of a conditional expression are:
·         argument expression in a function call
·         return expression
·          initializer in a member initialization list
·         object initialize
·         the right hand side operand of assignment (excluding compound assignment)
The last use is allowed on the basis of initialization of an object with automatic storage duration being equivalent to its declaration, followed by assignment.

Corresponding Ada rule:

Ada2012 provides two forms of conditional expression; the conditional if statement and the conditional case statement:

conditional_expression ::= if_expression | case_expression

    if_expression ::= 
   if 
condition then dependent_expression
   {elsif 
condition then dependent_expression}
   [else dependent_
expression]

condition ::= boolean_expression

   case_expression ::= 
    case selecting_
expression is
    
case_expression_alternative {,
    
case_expression_alternative}

case_expression_alternative ::= 
    when 
discrete_choice_list =>
        dependent_
expression

Wherever the Syntax Rules allow an expression, a conditional_expression may be used in place of the expression, so long as it is immediately surrounded by parentheses.

Note: In Ada assignment is not an expression. Each dependent expression of a condtional expression must be the type of the conditional expression.

6.     Statements

6.1.  Selection statements

6.1.1.     Enclose the body of a selection or an iteration statement in a compound statement

Follow each control flow primitive (if, else, while, for, do and switch) by a block enclosed by braces, even if the block is empty or contains only one line. Use of null statements or statement expressions in these contexts reduces code readability and making it harder to maintain.

Corresponding Ada rule:

All Ada selection or iteration statements are always fully bracketed compound statements.

6.1.2.     Explicitly cover all paths through multi-way selection statements

Make sure that each if-else-if chain has a final else clause, and every switch statement has a default clause.
The advantage is that all execution paths are explicitly considered, which in turn helps reduce the risk that an unexpected value will result in incorrect execution.

Corresponding Ada rule:

The rule for if-elsif-else in Ada applies.
Ada case statements require all possible values of the selection type to be covered. If some value or values are not covered explicitly then an others case must be included. The compiler will issue an error if this rule is not followed.

6.1.3.     Ensure that a non-empty case statement block does not fall through to the next label

Fall through from a non empty case block of a switch statement makes it more difficult to reason about the code, and therefore harder to maintain.

Corresponding Ada rule:

Ada case statements do not exhibit fall-through.

6.1.4.     Ensure that a switch statement has at least two case labels, distinct from the default label

A switch statement with fewer than two case labels can be more naturally expressed as a single if statement.

Corresponding Ada rule:

Ada case statements are intended to handle all the values of a discrete type, not just one. A simple if statement is appropriate if only one value of a discrete range of values is being tested.

6.2.  Iteration statements

6.2.1.     Implement a loop that only uses element values as a range-based loop

A range-based for statement reduces the amount of boilerplate code required to maintain correct loop semantics.
A range-based loop can normally replace an explicit loop where the index or iterator is only used for accessing the container value.

Corresponding Ada rule:

The traditional Ada for loop always iterates over a range of values. The Ada iterator loop iterates through the values of all standard Ada containers and all Ada arrays.

6.2.2.     Ensure that a loop has a single loop counter, an optional control variable, and is not degenerate

A loop is considered ’degenerate’ if:
·         when entered, the loop is infinite, or
·         the loop will always terminate after the first iteration.
To improve maintainability it is recommended to avoid degenerate loops and to limit them to a single counter variable.

Corresponding Ada rule:

Ada provides four kinds of loops.
·         The simple loop is the most general loop and can be used for loops that test termination at the beginning of the loop, at the end of the loop, or in the middle of the loop
·         The while loop tests a control expression at the top of the loop
·         The for loop executes for each value in a specified range
·         The iterator loop executes for every member of a container or array
Ada does not provide a direct equivalent of the C++ for loop.

6.2.3.     Do not alter the control or counter variable more than once in a loop

The behavior of iteration statements with multiple modifications of control or counter variables is difficult to understand and maintain.

Corresponding Ada rule:

The Ada for loop has a counter which is modified only at the top of the loop, and is read-only within the body of the loop. The counter cannot be modified more than once per loop iteration.

6.2.4.     Only modify a for loop counter in the for expression

It is expected that a for loop counter is modified for every iteration. To improve code readability and maintainability, the counter variable should be modified in the loop expression.

Corresponding Ada rule:

The Ada for loop has a counter which is modified only at the top of the loop, and is read-only within the body of the loop. The counter cannot be modified more than once per loop iteration.

6.3.  Jump statements

6.3.1.     Ensure that the label(s) for a jump statement or a switch condition appear later, in the same or an enclosing block

Backward jumps and jumps into nested blocks make it more difficult to reason about the flow through the function.
Loops should be the only constructs that perform backward jumps, and the only acceptable use of a goto statement is to jump forward to an enclosing block.
Control can also be transferred forward into a nested block by virtue of a switch label. Unless case and default labels are placed only into the top level compound statement of the switch, the code will be difficult to understand and maintain.

Corresponding Ada rule:

Ada case statements do not allow selection of a value within an if statement.
Ada loops can be labeled, and the loop exit statement in an inner loop can identify the label of an outer loop, allowing a direct exit from the outer loop controlled by a condition in an inner loop. This ability removes the need to set flag variables throughout a nesting of loops with associated testing of the flag. The advantage is code simplicity and added readability.

6.3.2.     Ensure that execution of a function with a non-void return type ends in a return statement with a value

Undefined behavior will occur if execution of a function with a non void return type (other than main) flows off the end of the function without encountering a return statement with a value.
Exception:

The main function is exempt from this rule, as an implicit return 0; will be executed, when an explicit return statement is missing.

Corresponding Ada rule:

Ada provides two kinds of subprograms. Procedures never return a value. Functions always return a value. The return value of a function must always be handled by the calling subprogram. The Ada compiler will issue an error if a function does not return a value of the specified return type, or if a procedure attempts to return a value of any type.
The Ada main subprogram is always a procedure and returns no value.

6.4.  Declaration statement

To preserve locality of reference, variables with automatic storage duration should be defined just before they are needed, preferably with an initializer, and in the smallest block containing all the uses of the variable.
The scope of a variable declared in a for loop initialization statement extends only to the complete for statement.
Therefore, potential use of a control variable outside of the loop is naturally avoided.

Corresponding Ada rule:

Ada only allows variables, constants, subprograms, or tasks to be defined in the declaration section of a block. The one exception is the for loop control variable which is defined at the start of the for loop and is not visible outside the for loop.

7.     Declarations

7.1.  Specifiers

7.1.1.     Declare each identifier on a separate line in a separate declaration

Declaring each variable or typedef on a separate line makes it easier to find the declaration of a particular identifier.
Determining the type of a particular identifier can become confusing for multiple declarations on the same line.
Exception:

For loop initialization statement is exempt from this rule, as in this context the rule conflicts with Rule 6.4.1: ”Postpone variable definitions as long as possible”, which takes precedence.

Corresponding Ada rule:

Ada only allows variables, constants, subprograms, or tasks to be defined in the declaration section of a block. The one exception is the for loop control variable which is defined at the start of the for loop and is not visible outside the for loop.

7.1.2.     Use const whenever possible

This allows specification of semantic constraint which a compiler can enforce. It explicitly communicates to other programmers that value should remain invariant. For example, specify whether a pointer itself is const, the data it points to is const, both or neither.
Exception:

By-value return types are exempt from this rule. These should not be const as doing so will inhibit move semantics.

Corresponding Ada rule:

Ada allows an object to be defined as constant, but does not have the complex const rules of C++. When passing parameters to a subprogram the passing mode can be defined as IN, which forces the subprogram to view that parameter as a constant. Parameters passed with the IN mode are read-only within the subprogram they are passed to.

7.1.3.     Do not place type specifiers before non-type specifiers in  declaration

The C++ Standard allows any order of specifiers in a declaration. However, to improve readability if a non-type specifier (typedef, friend, constexpr, register, static, extern, thread local, mutable, inline, virtual, explicit) appears in a declaration, it should be placed leftmost in the declaration.

Corresponding Ada rule:

Variable declarations always take the form of
object_declaration ::= 
    
defining_identifier_list : [aliased] [constantsubtype_indication [:= expression]
        [
aspect_specification];
  | 
defining_identifier_list : [aliased] [constantaccess_definition [:= expression]
        [
aspect_specification];
  | 
defining_identifier_list : [aliased] [constantarray_type_definition [:= expression]
        [
aspect_specification];
  | 
single_task_declaration
  | 
single_protected_declaration

defining_identifier_list ::= 
  
defining_identifier {, defining_identifier}

7.1.4.     Place CV-qualifiers on the right hand side of the type they apply to

The const or volatile qualifiers can appear either to the right or left of the type they apply to. When the unqualified portion of the type is a typedef name (declared in a previous typedef declaration), placing the CV-qualifiers on the left hand side, may result in confusion over what part of the type the qualification applies to.
For consistency, it is recommended that this rule is applied to all declarations.

Corresponding Ada rule:

Variable declarations always take the form of
object_declaration ::= 
    
defining_identifier_list : [aliased] [constantsubtype_indication [:= expression]
        [
aspect_specification];
  | 
defining_identifier_list : [aliased] [constantaccess_definition [:= expression]
        [
aspect_specification];
  | 
defining_identifier_list : [aliased] [constantarray_type_definition [:= expression]
        [
aspect_specification];
  | 
single_task_declaration
  | 
single_protected_declaration

defining_identifier_list ::= 
  
defining_identifier {, defining_identifier}

7.1.5.     Do not inline large functions

The definition of an inline function needs to be available in every translation unit that uses it. This in turn requires that the definitions of inline functions and types used in the function definition must also be visible.
The inline keyword is just a hint, and compilers in general will only inline a function body if it can be determined that performance will be improved as a result.
As the compiler is unlikely to inline functions that have a large number of statements and expressions, inlining such functions provides no performance benefit but will result in increased dependencies between translation units.
Given an approximate cost of 1 for every expression and statement, the recommended maximum cost for a function is 32.

Corresponding Ada rule:

The inline aspect of a subprogram is optional for a compiler. It notifies the compiler of the programmer’s desire that the subprogram be inlined.

7.1.6.     Use class types or typedefs to abstract scalar quantities and standard integer types

Using class types to represent scalar quantities exploits compiler enforcement of type safety. If this is not possible, typedefs should be used to aid readability of code.
Plain char type should not be used to define a typedef name, unless the type is intended for parameterizing the code for narrow and wide character types. In other cases, an explicit signed char or unsigned char type should be used in a typedef as appropriate.
To enhance portability, instead of using the standard integer types (signed char, short, int, long, long long, and the unsigned counterparts), size specific types should be defined in a project-wide header file, so that the definition can be updated to match a particular platform (16, 32 or 64bit). Where available, intN t and uintN t types (e.g. int8 t) defined in the cstdint header file should be used for this purpose.
Where the auto type specifier is used in a declaration, and the initializer is a constant expression, the declaration should not be allowed to resolve to a standard integer type. The type should be fixed by casting the initializer to a size specific type.
Exception:

The C++ Language Standard places type requirements on certain constructs. In such cases, it is better to use required type explicitly rather than the typedef equivalent which would reduce the portability of the code.
The following constructs are therefore exceptions to this rule:
·         int main()

·         T operator++(int)

·         T operator–(int)

Corresponding Ada rule:

Ada allows the programmer to create simple and detailed definitions of scalar types and subtypes. New integer types can be simply defined by creating a type name and specifying the valid range of values for the type, or they can be derived from an existing type with a restriction on the range of valid values.

For example:

type Counts is range 0..100;

type Normalized is new Integer range -100..100;

Ada also allows the programmer to define subtypes of existing types. Objects of a subtype are members of the base type, usually with a restricted range of valid values.

subtype Natural is Integer range 0..Integer’Last;

subtype Positive is Integer range 1..Integer’Last;

Subtypes can be used in mixed calculations with any other subtypes of the same base type. Objects of different base types cannot be used in an expression without explicit conversion to a common type.

7.1.7.     Use a trailing return type in preference to type disambiguation using typename

When using a trailing return type, lookup for the function return type starts from the same scope as the function declarator. In many cases, this will remove the need to specify a fully qualified return type along with the typename keyword.

Corresponding Ada rule:

Ada has no equivalent of a trailing return type.

7.1.8.     Use auto id = expr when declaring a variable to have the same type as its initializer function call

When declaring a variable that is initialized with a function call, the type is being specified twice. Initially on the return of the function and then in the type of the declaration.
Using auto and implicitly deducing the type of the initializer will ensure that a future change to the declaration of foo will not result in the addition of unexpected implicit conversions.

Corresponding Ada rule:

This rule is contrary to Ada strong typing. Every variable must be declared to be definite, even if it is a member of an indefinite type. The type returned by an initializing function must match the type of the object being declared.
An object of an indefinite type must be initialized to a definite object. For instance, the type String is an unconstrained array of character. An unconstrained array is an indefinite type. Each instance of a String must have a specified length, which is defined when the String object is initialized. The String object is definite. The String type is indefinite.

7.1.9.     Do not explicitly specify the return type of a lambda

Allowing the return type of a lambda to be implicitly deduced reduces the danger of unexpected implicit conversions, as well as simplifying future maintenance, where changes to types used in the lambda would otherwise result in the need to change the return type.

Corresponding Ada rule:

Ada does not allow lambdas.

7.1.10.                        Use static_assert for assertions involving compile time constants

A static assert will generate a compile error if its expression is not true. The earlier that a problem can be diagnosed the better, with the earliest time possible being as the code is written.

Corresponding Ada rule:

When defining a subtype the programmer should use either a Dynamic_Predicate or a Static_Predicate if the use of a simple range cannot fully define the characteristics of the subtype.
A Static_Predicate expression must be one of
·         A static membership test where the choice is selected by the current instance
·         A case expression whose dependent expressions are static and selected by the current instance
·         A call of the predefined operations =, /=, <, <=, >, >= where one operand is the current instance
·         An ordinary static expression
A Dynamic_Expression can be any Boolean expression.
Examples:

subtype Even is Integer

   with Dynamic_Predicate => Even mod 2 = 0;


subtype Letter is Character

   with Static_Predicate => Letter in ‘A’..’Z’|’a’..’z’;

7.2.  Enumeration declarations

7.2.1.     Use an explicit enumeration base and ensure that it is large enough to store all enumerations

The underlying type of an unscoped enumeration is implementation defined, with the only restriction being that the type must be able to represent the enumeration values. An explicit enumeration base should always be specified with a type that will accommodate both the smallest and the largest enumerator.
A scoped enum will implicitly have an underlying type of int, however, the requirement to specify the underlying type still applies.
Exception:

An enumeration declared in an extern "C" block (i.e. one intended to be used with C) does not require an explicit underlying type.

Corresponding Ada rule:

Ada enumerations are not numeric types and cannot be converted to numeric types. The compiler chooses the size of the enumeration representation base upon the number of enumeration values.

7.2.2.     Initialize none, the first only, or all enumerators in an enumeration

It is error prone to initialize explicitly only some enumerators in an enumeration, and to rely on the compiler to initialize the remaining ones. For example, during maintenance it may be possible to introduce implicitly initialized enumerators with the same value as an existing one initialized explicitly.
Exception:

When an enumeration is used to define the size and to index an array, it is acceptable and recommended to define three additional enumerators after all other enumerators, to represent the first and the last elements, and the size of the array.

Corresponding Ada rule:

When specifying the enumeration representation all enumerated values must be specified.
Example:
type Mix_Code is (ADD, SUB, MUL, LDA, STA, STZ);

for Mix_Code use (ADD => 1, SUB => 2, MUL => 3, LDA => 8, STA => 24, STZ =>33);

7.3.  Namespaces

7.3.1.     Do not use using directives

Namespaces are an important tool in separating identifiers and in making interfaces explicit.
A using directive, i.e. using namespace, allows any name to be searched for in the namespace specified by the using directive.
A using declaration, on the other hand, brings in a single name from the namespace, as if it was declared in the scope containing the using declaration.

Corresponding Ada rule:

The unit of encapsulation in Ada is the package. Packages provide the name space designation of C++ namespaces.
In Ada one can either use the entire package, use only a specific type defined in the package, or explicitly append the package name to each element accessed from the package.
Any naming ambiguity will be identified by the compiler.

7.4.  Linkage specifications

7.4.1.     Ensure that any objects, functions or types to be used from a single translation unit are defined in an unnamed namespace in the main source file

Declaring an entity in an unnamed namespace limits its visibility to the current translation unit only. This helps reduce the risk of name clashes and conflicts with declarations in other translation units.
It is preferred to use unnamed namespaces rather than the static keyword to declare such entities.

Corresponding Ada rule:

Each compilation unit specifies its own dependencies in its dependency clauses. There is no visibility to compilation units not defined in the dependency clause.

7.4.2.     Ensure that an inline function, a function template, or a type used from multiple translation units is defined in a single header file

An inline function, a function template or a user defined type that is intended for use in multiple translation units should be defined in a single header file, so that the definition will be processed in exactly the same way (the same sequence of tokens) in each translation unit.
This will ensure that the one definition rule is adhered to, avoiding undefined behavior, as well as improving the maintainability of the code.

Corresponding Ada rule:

Each subprogram, generic compilation unit, type, task, task type, protected object, or protected type is defined uniquely in a compilation unit. Dependency upon compilation units which define types or objects with overlapping identifiers results in ambiguity which is flagged as a compilation error and must be corrected before an executable is produced by the compilation process.

7.4.3.     Ensure that an object or a function used from multiple translation units is declared in a single header file

An object or function with external linkage should be declared in a single header file in the project.
This will ensure that the type seen for an entity in each translation unit is the same thereby avoiding undefined behavior.

Corresponding Ada rule:

Objects used by multiple compilation units must be declared in the public region of a package. Functions and procedures can either be declared in a package or in a stand-alone compilation unit.

7.5.  The asm declaration

7.5.1.     Do not use the asm declaration

Use of inline assembly should be avoided since it restricts the portability of the code.

Corresponding Ada rule:

Machine code insertions should be avoided since they restrict the portability of the code.

8.     Definitions

8.1. Type names

8.1.1.     Do not use multiple levels of pointer indirection

In C++, at most one level of pointer indirection combined with references is sufficient to express any algorithm or API.
Instead of using multidimensional arrays, an array of containers or nested containers should be used. Code reliant on more than one level of pointer indirection will be less readable and more difficult to maintain.

Corresponding Ada rule:

Use of multidimensional arrays is allowed, or one can choose containers or nested containers. Ada arrays do not employ pointer indirection at the source code level.

8.2. Meanings of declarators

8.2.1.     Make parameter names absent or identical in all declarations

Although the C++ Standard does not mandate that parameter names match in all declarations of a function (e.g. a declaration in a header file and the definition in the main source file), it is good practice to follow this principle.

Corresponding Ada rule:

All subprogram parameter names must be declared and must match the corresponding subprogram implementation.
Subprogram parameters may be referenced by position or by name when the subprogram is called. It is recommended to use named notation when calling a subprogram. When using named notation the actual parameter is explicitly matched with the formal parameter, and the order of parameters in the called subprogram is not relevant.
Example:
Procedure Get_Line(Item : out String; Last : out Natural);

Length : Natural;

Input : String(1..256);

Get_Line(Item => Input, Last => Length);

8.2.2.     Do not declare functions with excessive number of parameters

A function defined with a long list of parameters often indicates poor design and is difficult to read and maintain.
The recommended maximum number of function parameters is six.

Corresponding Ada rule:

While it is seldom useful to declare a subprogram with a large number of parameters, the problem is greatly relieved through the use of named notation.

8.2.3.     Pass small objects with a trivial copy constructor by value

Because passing by const reference involves an indirection, it will be less efficient than passing by value for a small object with a trivial copy constructor.

Corresponding Ada rule:

Ada parameters have an associated mode indicating data flow. The modes are IN, OUT, IN OUT. The compiler determines whether a particular parameter should be passed by copy or by reference.

8.2.4.     Do not pass std::unique_ptr by const reference

An object of type std::unique ptr should be passed as a non-const reference, or by value. Passing by non-const reference signifies that the parameter is an in/out parameter. Passing by value signifies that the parameter is a sink (i.e. takes ownership and does not return it).
A const reference std::unique ptr parameter provides no benefits and restricts the potential callers of the function.

Corresponding Ada rule:

Ada parameter modes explicitly state whether the parameter is passed IN, OUT, or IN OUT.

8.3.  Function definitions

8.3.1.     Do not write functions with excessive McCabe Cyclomatic Complexity

The McCabe Cyclomatic Complexity is calculated as the number of decision branches within a function plus 1.
Complex functions are hard to maintain and test effectively. It is recommended that the value of this metric does not exceed 10.

Corresponding Ada rule:

Excessive cyclomatic complexity has been shown to produce code that is difficult to understand and maintain. It is often an indication of poor code design.

8.3.2.     Do not write functions with a high static program path count

Static program path count is the number of non-cyclic execution paths in a function. Functions with a high number of paths through them are difficult to test, maintain and comprehend. The static program path count of a function should not exceed 200.

Corresponding Ada rule:

High static path count is an indication of poor software design. All code should be testable and maintainable.

8.3.3.     Do not use default arguments

Use of default arguments can make code maintenance and refactoring more difficult. Overloaded forwarding functions can be used instead without having to change existing function calls.

Corresponding Ada rule:

Overloaded forwarding functions provide no maintenance benefit. Use of default arguments and named notation for calling subprograms clearly documents all calls for maintenance purposes without creating a plethora of overloaded subprograms.

8.3.4.     Define =delete functions with parameters of rvalue reference to const

A simple model for an rvalue reference is that it allows for the modification of a temporary. A const rvalue reference therefore defeats the purpose of the construct as modifications are not possible.
However, one valid use case is where the function is defined =delete. This will disallow the use of an rvalue as an argument to that function.

Corresponding Ada rule:

Ada has no equivalent to defining a subprogram as =delete.

8.4.  Initializers

8.4.1.     Do not access an invalid object or an object with indeterminate value

A significant component of program correctness is that the program behavior should be deterministic. That is, given the same input and conditions the program will produce the same set of results.
If a program does not have deterministic behavior, then this may indicate that the source code is reliant on unspecified or undefined behavior.
Such behaviors may arise from use of:
·         variables not yet initialized
·         memory (or pointers to memory) that has been freed
·         moved from objects

Corresponding Ada rule:

The Ada compiler identifies when the value of a variable is used before the variable is initialized or assigned a value. The SPARK subset of Ada requires all variables to be fully initialized.

8.4.2.     Ensure that a braced aggregate initialize matches the layout of the aggregate object

If an array or a struct is non-zero initialized, initializers should be provided for all members, with an initializer list for each aggregate (sub)object enclosed in braces. This will make it clear what value each member is initialized with.

Corresponding Ada rule:

Aggregates can be constructed for arrays or records. While aggregates can be constructed using positional notation, similar to C++, named notation should always be used.
Use of named notation will ensure and document that all elements of the composite type have been initialized.

9.     Classes

9.1.  Member functions

9.1.1.     Declare static any member function that does not require this. Alternatively, declare const any member function that does not modify the externally visible state of the object

A non-virtual member function that does not access the this pointer can be declared static. Otherwise, a function that is virtual or does not modify the externally visible state of the object can be declared const.
The C++ language permits that a const member function modifies the program state (e.g. modifies a global variable, or calls a function that does so). However, it is recommended that const member functions are logically const also, and do not cause any side effects.
The mutable keyword can be used to declare member data that can be modified in a const function, however, this should only be used where the member data does not affect the externally visible state of the object.

Corresponding Ada rule:

Packages are the unit of encapsulation in Ada. Tagged types are polymorphic types in Ada. Tagged types can be defined in a package, making them available to multiple compilation units. Tagged types can have primitive operations, which correspond to virtual member functions in C++. A subprogram is primitive to some tagged type T if all of the following are true:
·         The subprogram is declared in the visible part of the package in which tagged type T is declared
·         The subprogram has a parameter of type T, or an access parameter pointing to an instance of type T, or is a function returning a result of type T
Any subprogram which is declared in the package where type T is declared, and does not meet the above requirements, is not a primitive of type T. Such a subprogram is equivalent to a C++ static member function. Any procedure with only an parameter of IN mode of type T is equivalent to a C++ const member function.

9.1.2.     Make default arguments the same or absent when overriding a virtual function

The C++ Language Standard allows that default arguments be different for different overrides of a virtual function.
However, the compiler selects the argument value based on the static type of the object used in the function call.
This can result in confusion where the default argument value used may be different to the expectation of the user.

Corresponding Ada rule:

The Ada Language Standard requires  overridden subprograms be conformant in their use of default parameter expressions. When a subprogram overrides another subprogram the parameter profile cannot change regarding default expressions.

9.1.3.     Do not return non-const handles to class data from const member functions

A pointer or reference to non-const data returned from a const member function may allow the caller to modify the state of the object. This contradicts the intent of a const member function.

Corresponding Ada rule:

A procedure with an IN mode parameter  cannot modify the data passed to it, nor can it return a value of any kind.

9.1.4.     Do not write member functions which return non-const handles to data less accessible than the member function

Member data that is returned by a non-const handle from a more accessible member function, implicitly has the access of the function and not the access it was declared with. This reduces encapsulation and increases coupling.
Exception:

Non-const operator [] is exempt from this rule, as in this context the rule conflicts with Rule 13.2.4: ”When overloading the subscript operator (operator[]) implement both const and non-const versions”, which takes precedence.

Corresponding Ada rule:

Ada functions need not return pointers to complex data. Instead they can return entire complex objects including records and arrays. This avoids the lifetime issues associated with C++ member functions.

9.1.5.     Do not introduce virtual functions in a final class

Declaring a class as final explicitly documents that this is a leaf class as it cannot be used as a base class.
Introducing a virtual function in such a class is therefore redundant as the function can never be overridden in a derived class.

Corresponding Ada rule:

Ada does not explicitly label a tagged type “final”. Instead, if no subprograms with class-wide parameters are introduced the tagged type has the effect of being “final”.

9.2.  Bit-fields

9.2.1.     Declare bit-fields with an explicitly unsigned integral or enumeration type

To avoid reliance on implementation defined behavior, only declare bit-fields of an explicitly unsigned type (uintN t) or an enumeration type with an enumeration base of explicitly unsigned type.

Corresponding Ada rule:

One must define data types with value ranges that can fit into the specified bit layout of a record. Failure to do so will result in a compiler error message.
Example:
Word : constant := 4;  --  storage element is byte, 4 bytes per word

type State         is (A,M,W,P);
type Mode          is (Fix, Dec, Exp, Signif);

type Byte_Mask     is array (0..7)  of Boolean;
type State_Mask    is array (State) of Boolean;
type Mode_Mask     is array (Mode)  of Boolean;

type Program_Status_Word is
  record
      System_Mask        : Byte_Mask;
      Protection_Key     : Integer range 0 .. 3;
      Machine_State      : State_Mask;
      Interrupt_Cause    : Interruption_Code;
      Ilc                : Integer range 0 .. 3;
      Cc                 : Integer range 0 .. 3;
      Program_Mask       : Mode_Mask;
      Inst_Address       : Address;
end record;

for Program_Status_Word use
  record
      System_Mask      at 0*Word range 0  .. 7;
      Protection_Key   at 0*Word range 10 .. 11; --
 bits 8,9 unused
      Machine_State    at 0*Word range 12 .. 15;
      Interrupt_Cause  at 0*Word range 16 .. 31;
      Ilc              at 1*Word range 0  .. 1;  --
 second word
      Cc               at 1*Word range 2  .. 3;
      Program_Mask     at 1*Word range 4  .. 7;
      Inst_Address     at 1*Word range 8  .. 31;
  end record;

for Program_Status_Word'Size use 8*System.Storage_Unit;
for Program_Status_Word'Alignment use 8;

10.           Derived classes

10.1.                 Multiple base classes

10.1.1.                        Ensure that access to base class subobjects does not require explicit disambiguation

A class inherited more than once in a hierarchy, and not inherited virtually in all paths will result in multiple base class subobjects being present in instances of the derived object type.
Such objects require that the developer explicitly select which base class to use when accessing members. The result is a hierarchy that is harder to understand and maintain.

Corresponding Ada rule:

Ada allows multiple inheritance only of interfaces, not of tagged types. All subprograms in interfaces are abstract, corresponding to virtual inheritance of multiple base classes in C++.

10.2.                 Virtual functions

10.2.1.                        Use the override special identifier when overriding a virtual function

The override special identifier is a directive to the compiler to check that the function is overriding a base class member. This will ensure that a change in the signature of the virtual function will generate a compiler error.

Corresponding Ada rule:

An overriding_indicator is used to declare that an operation is intended to override (or not override) an inherited operation. 

Syntax

overriding_indicator ::= [notoverriding

Legality Rules

·        the operation shall be a primitive operation for some type;

·        if the overriding_indicator is overriding, then the operation shall override a homograph at the place of the declaration or body;

·        if the overriding_indicator is not overriding, then the operation shall not override any homograph (at any place). 

In addition to the places where Legality Rules normally apply, these rules also apply in the private part of an instance of a generic unit.

10.3.                 Abstract classes

10.3.1.                        Ensure that a derived class has at most one base class which is not an interface class

An interface class has the following properties:
·         all public functions are pure virtual functions or getters, and
·         there are no public or protected data members, and
·          it contains at most one private data member of integral or enumerated type
Inheriting from two or more base classes that are not interfaces, is rarely correct. It also exposes the derived class to multiple implementations, with the risk that subsequent changes to any of the base classes may invalidate the derived class.
On the other hand. it is reasonable that a concrete class may implement more than one interface.

Corresponding Ada rule:

Ada only allows inheritance from one base class. Ada allows multiple inheritance of interfaces.

11.           Member access control

11.1.                 Access specifiers

11.1.1.                        Declare all data member private

If direct access to the object state is allowed through public or protected member data, encapsulation is reduced making the code harder to maintain.
By implementing a class interface with member functions only, precise control is achieved over modifications to object state as well as allowing for pre and post conditions to be checked when accessing data.

Corresponding Ada rule:

There are many uses for packages. One use is to define commonly used constants, while another is to define abstract data types. It is good to use data hiding for abstract data types, but it is also important to openly share commonly used constants. When declaring abstract data types in Ada it is advised that the public view of the data type refers to a private definition of the data structure.

11.2.                 Friends

11.2.1.                        Do not use friend declarations

Friend declarations reduce encapsulation, resulting in code that is harder to maintain.

Corresponding Ada rule:

Ada provides child packages rather than friends. Child packages do not reduce encapsulation. Child packages allow the creation of package extensions without compromising or altering the code in the parent package.

12.           Special member functions

12.1.                 Conversions

12.1.1.                        Do not declare implicit user defined conversions

A user defined conversions can occur through the use of a conversion operator or a conversion constructor (a constructor that accepts a single argument).
A compiler can invoke a single user defined conversion in a standard conversion sequence, but only if the operator or constructor is declared without the explicit keyword.
It is better to declare all conversion constructors and operators explicit.

Corresponding Ada rule:

Ada does not perform implicit conversions.

12.2.                 Destructors

12.2.1.                        Declare virtual, private, or protected the destructor of a type used as a base class

If an object will ever be destroyed through a pointer to its base class, then the destructor in the base class should be virtual. If the base class destructor is not virtual, then the destructors for derived classes will not be invoked.
Where an object will not be deleted via a pointer to its base, then the destructor should be declared with protected or private access. This will result in a compile error should an attempt be made to delete the object incorrectly.

Corresponding Ada rule:

Ada controlled types allow user-defined finalization of objects of the type. Finalization is called when an object goes out of scope.
Ada polymorphism does not require the use of pointers to a base class.

12.3.                 Free store

12.3.1.                        Correctly declare overloads for operator new and delete

operator new and operator delete should work together. Overloading operator new means that a custom memory management scheme is in operation for a particular class or program. If a corresponding operator delete (plain or array) is not provided the memory management scheme is incomplete.
Additionally, if initialization of the allocated object fails with an exception, the C++ runtime will try to call an operator delete with identical parameters as the called operator new, except for the first parameter. If no such operator delete can be found, the memory will not be freed. If this operator delete does not actually need to perform any bookkeeping, one with an empty body should be defined to document this in the code.
When declared in a class, operator new and operator delete are implicitly static members; explicitly including the static specifier in their declarations helps to document this.

Corresponding Ada rule:

When creating a custom storage pool one must override the abstract type Root_Storage_Pool, including overriding its Allocate and Deallocate procedures and its Storage_Size function. There is no way to implement a custom storage pool without implementing all three subprograms.

12.4.                 Initializing bases and members

12.4.1.                        Do not use the dynamic type of an object unless the object is fully constructed

Expressions involving:
·         a call to a virtual member function,
·         use of typeid, or
·         a cast to a derived type using dynamic cast
are said to use the dynamic type of the object.
Special semantics apply when using the dynamic type of an object while it is being constructed or destructed. Moreover, it is undefined behavior if the static type of the operand is not (or is not a pointer to) the constructor’s or destructor’s class or one of its base classes.
In order to avoid misconceptions and potential undefined behavior, such expressions should not be used while the object is being constructed or destructed.

Corresponding Ada rule:

Ada has no concept of a dynamic type, only dynamic type identification. Ada can perform view conversions of a child type to its parent type, but that does not change the type of an object.

12.4.2.                        Ensure that a constructor initializes explicitly all base classes and non-static data members

A constructor should completely initialize its object. Explicit initialization reduces the risk of an invalid state after successful construction. All virtual base classes and direct non-virtual base classes should be included in the initialization list for the constructor. A copy or move constructor should initialize each non-static data member in the initialization list, or if this is not possible then in constructor body. For other constructors, each non-static data member should be initialized in the following way, in order of preference:
·         non static data member initializer (NSDMI), or
·         in initialization list, or
·         in constructor body.
For many constructors this means that the body becomes an empty block.

Corresponding Ada rule:

Ada records, including tagged records, can be defined with default values for all their record components. The default initialization of a parent tagged record is applied to the child tagged record so that the child need not explicitly call parent initialization functions.

12.4.3.                        Do not specify both an NSDMI and a member initialize for the same non-static member

NSDMI stands for ’non static data member initializer’. This syntax, introduced in the 2011 C++ Language Standard, allows for the initializer of a member to be specified along with the declaration of the member in the class body. To avoid confusion as to the value of the initializer actually used, if a member has an NSDMI then it should not subsequently be initialized in the member initialization list of a constructor.

Corresponding Ada rule:

Ada does not provide for multiple initialization of an object.

12.4.4.                        Write members in an initialization list in the order in which they are declared

Regardless of the order of member initializers in a initialization list, the order of initialization is always:
·         Virtual base classes in depth and left to right order of the inheritance graph.
·         Direct non-virtual base classes in left to right order of inheritance list.
·         Non-static member data in order of declaration in the class definition.
To avoid confusion and possible use of uninitialized data members, it is recommended that the initialization list matches the actual initialization order.

Corresponding Ada rule:

Since Ada only allows single inheritance from a base class there is no issue about which base class is first initialized.

12.4.5.                        Use delegating constructors to reduce code duplication

Delegating constructors can help reduce code duplication by performing initialization in a single constructor. Using delegating constructors also removes a potential performance penalty with using an ’init’ method, where initialization for some members occurs twice.

Corresponding Ada rule:

Ada does not provide explicit constructors. Ada does provide an Initialize procedure for controlled types.

12.5.                 Copying and moving class objects

12.5.1.                        Define explicitly =default or =delete implicit special member functions of concrete classes

A compiler may provide some or all of the following special member functions:
·         Destructor
·         Copy constructor
·         Copy assignment operator
·         Move constructor
·         Move assignment operator
The set of functions implicitly provided depends on the special member functions that have been declared by the user and also the special members of base classes and member objects.
The compiler generated versions of these functions perform a bitwise or shallow copy, which may not be the correct copy semantics for the class. It is also not clear to clients of the class if these functions can be used or not.
To resolve this, the functions should be defined with =delete or =default thereby fully documenting the class interface.
Note: As this rule is limited to concrete classes, it is the responsibility of the most derived class to ensure that the object has correct copy semantics for itself and for its sub-objects.

Corresponding Ada rule:

Tagged types inheriting from Controlled types must define Initialize, Adjust, and Finalize procedures to handle initialization, copy semantics, and deletion semantics.

12.5.2.                        Define special members =default if the behavior is equivalent

Corresponding Ada rule:

This rule has no Ada equivalent because Ada prohibits inheritance from multiple base classes.

12.5.3.                        Ensure that a user defined move/copy constructor only moves/copies base and member objects

The human clients of a class will expect that the copy constructor can be used to correctly copy an object of class type. Similarly, they will expect that the move constructor correctly moves an object of class type.
Similarly, a compiler has explicit permission in the C++ Standard to remove unnecessary copies or moves, on the basis that these functions have no other side-effects other than to copy or move all bases and members.

Corresponding Ada rule:

Tagged types inheriting from Controlled types must define Initialize, Adjust, and Finalize procedures to handle initialization, copy semantics, and deletion semantics.

12.5.4.                        Declare noexcept the move constructor and move assignment operator

A class provides the Strong Exception Guarantee if after an exception occurs, the objects maintain their original values.
The move members of a class explicitly change the state of their argument. Should an exception be thrown after some members have been moved, then the Strong Exception Guarantee may no longer hold as the from object has been modified.
It is especially important to use noexcept for types that are intended to be used with the standard library containers.
If the move constructor for an element type in a container is not noexcept then the container will use the copy constructor rather than the move constructor.

Corresponding Ada rule:

Ada has no equivalent to a move constructor. A move constructor moves ownership of data from one object to another. If the resource is accessed through an access value then the access value of the data in the starting object must be copied to the corresponding access field of the target object. After the access value is copied the access value in the starting object must be set to null.

12.5.5.                        Correctly reset moved-from handles to resources in the move constructor

The move constructor moves the ownership of data from one object to another. Once a resource has been moved to a new object, it is important that the moved-from object has its handles set to a default value. This will ensure that the moved-from object will not attempt to destroy resources that it no longer manages on its destruction.
The most common example of this is to assign nullptr to pointer members.

Corresponding Ada rule:

Ada has no equivalent to a move constructor. A move constructor moves ownership of data from one object to another. If the resource is accessed through an access value then the access value of the data in the starting object must be copied to the corresponding access field of the target object. After the access value is copied the access value in the starting object must be set to null.

12.5.6.                        Use and atomic, non-throwing swap operation to implement the copy and move assignment operators

Implementing the copy assignment operator using a non throwing swap provides the Strong Exception Guarantee for the operations.
In addition, the implementation of each assignment operator is simplified without requiring a check for assignment to self.

Corresponding Ada rule:

Use of an atomic swap operation assumes the use of pointers and not full objects. This assumption frequently fails since Ada does not require the use of pointers to complex data types.

12.5.7.                        Declare assignment operators with the ref-qualifier &

In the 2003 C++ Language Standard, user declared types differed from built-in types in that it was possible to have a ’modifiable rvalue’.
The 2011 C++ Language Standard allows for a function to be declared with a reference qualifier. Adding & to the function declaration ensures that the call can only be made on lvalue objects, as is the case for the built-in operators.

Corresponding Ada rule:

Ada does not allow modifiable rvalues.

12.5.8.                        Make the copy assignment operator of an abstract class protected ore define it =delete

An instance of an abstract class can only exist as a subobject for a derived type. A public copy assignment operator would allow for incorrect partial assignments to occur.
The copy assignment operator should be protected, or alternatively defined =delete if copying is to be prohibited in this class hierarchy.

Corresponding Ada rule:

Ada does not allow instances of abstract types. Each instance must be a concrete type derived from the abstract type.

13.           Overloading

13.1.                 Overload resolution

13.1.1.                        Ensure that all overloads of functions are visible from where it is called

When a member function is overridden or overloaded in a derived class, other base class functions of that name will be hidden. A call to a function from the derived class may therefore result in a different function being called than if the same call had taken place from the base class.
To avoid this situation, hidden names should be introduced into the derived class through a using declaration.
A using declaration for a namespace scope identifier, only brings into the current scope the prior declarations of this identifier, and not any declarations subsequently added to the namespace. This too may lead to unexpected results for calls to overloaded functions.

Corresponding Ada rule:

If the programmer wants the base type version of an overloaded or overridden subprogram to be called then the object must be passed to the overloaded subprogram name in the form of a view conversion to the base type.

13.1.2.                        If a member of a set of callable functions includes a universal reference parameter, ensure that one appears in the same position for all other members

A callable function is one which can be called with the supplied arguments. In the C++ Language Standard, this is known as the set of viable functions.
A template parameter declared T&& has special rules during type deduction depending on the value category of the argument to the function call. Scott Meyers has named this a ’Universal Reference’.
As a universal reference will deduce perfectly for any type, overloading them can easily lead to confusion as to which function has been selected.
Exception:

Standard C++ allows for a member of the viable function set to be deleted. In such cases, should these functions be called then it will result in a compiler error.

Corresponding Ada rule:

Ada does not provide an equivalent to universal reference. The closest Ada comes is an class-wide access type, which can reference any object in the inheritance hierarchy rooted at the tagged type specified in the definition of the access type.

13.2.                 Overloaded operators

13.2.1.                        Do not overload operators with special semantics

Overloaded operators are just functions, so the order of evaluation of their arguments is unspecified. This is contrary to the special semantics of the following built-in operators:
·         && – left to right and potentially evaluated
·          || – left to right and potentially evaluated
·          , – left to right
Providing user declared versions of these operators may lead to code that has unexpected behavior and is therefore harder to maintain.
Additionally, overloading the unary & (address of) operator will result in undefined behavior if the operator is used from a location in the source where the user provided overload is not visible.

Corresponding Ada rule:

The C++ operators && and || correspond to the Ada logical expressions “and then” and “or else”. Those logical expressions cannot be overloaded in Ada. Ada has no “address of” operator. It does have an attribute ‘Access which cannot be overloaded.

13.2.2.                        Ensure that the return type of an overloaded binary operator matches the built-in counterparts

Built-in binary arithmetic and bitwise operators return a pure rvalue (which cannot be modified), this should be mirrored by the overloaded versions of these operators. For this reason the only acceptable return type is a fundamental or an enumerated type or a class type with a reference qualified assignment operator.
Built-in equality and relational operators return a boolean value, and so should the overloaded counterparts.

Corresponding Ada rule:

Custom operators can be defined which return complex values. For instance:
type Matrix is array(Natural range 1..10, Natural range 1..10) of float;

type Vector is array(Natural range 1..10) of float;

function “+” (Left, Right : Matrix) return Matrix;

function “+” (Left, Right : Vector) return Vector;

In the case above addition operators are declared for types Matrix and Vector. In this case the acceptable return type is not a scalar and need not be a tagged type.

13.2.3.                        Declare binary arithmetic and bitwise operators as non-members

Overloaded binary arithmetic and bitwise operators should be non-members to allow for operands of different types, e.g. a fundamental type and a class type, or two unrelated class types.

Corresponding Ada rule:

Overloaded binary arithmetic operators should be declared within the package that a particular numeric type is declared. Integer or real types are often defined as part of a larger abstraction such as date and time utilities. Often achieving the same effect in C++ may take the creation of several classes.

13.2.4.                        When overloading the subscript operator (operator[]) implement both const and non-const versions

A non-const overload of the subscript operator should allow an object to be modified, i.e. should return a reference to member data. The const version is there to allow the operator to be invoked on a const object.

Corresponding Ada rule:

There is no subscript operator in Ada. Ada requires the programmer to specify the scalar subtype used to index an array type. Array indices in Ada may be indexed by any scalar type (signed integer, modular integer, enumeration) and the lowest index value may be set to any valid value of the specified index subtype.
Examples:
Type Days is (Mon, Tues, Wed, Thu, Fri, Sat, Sun);

Type Weekly_Sales is array(Days) of float;

This array’s index values start at Mon and end at Sun. Each array element is a float value.

Type Mod_Index is mod 10;

Type Circular_Array is array(Mod_Index) of message;

This array is indexed with a modular type. Modular types exhibit wrap around arithmetic, allowing this array to easily implement a circular message buffer.

Subtype Normalized_Index is Integer range -100..100;

Type Normal_Distribution is array(Normalized_Index) of Count;

This array can be used to count the frequency of some event plotted to a normal curve.
Ada does allow indexing operators to be defined for tagged types. This indexing ability in Ada helps greatly in the implementation of indexable container types such as maps and sets. The container type can be designated to employ either constant indexing or variable indexing. The aspect of the type declaring either constant indexing or variable indexing must indicate one or more functions taking two parameters, one of which is an instance of the container type or a reference to a container instance.
Ada user defined indexing is not used for array types, which are not tagged types.

13.2.5.                        Implement a minimal set of operators and use them to implement all other related operators

In order to limit duplication of code and associated maintenance overheads, certain operators can be implemented in terms of other operators.

Corresponding Ada rule:

The Ada 2012 Language Reference Manual states that one is not allowed to overload “/=” directly, however overloading “=” implicitly overloads “/=”, since not equals is simply the complement of the “=” function.

14.           Templates

14.1.                 Template declarations

14.1.1.                        Using variadic templates rather than ellipsis

Use of the ellipsis notation ... to indicate an unspecified number of arguments should be avoided. Variadic templates offer a type-safe alternative.

Corresponding Ada rule:

The Ada equivalent to a template is called a generic. A programmer can define a generic subprogram or a generic package. Each generic unit definition includes a set of generic parameters followed by a normal subprogram or package specification.
There is no need for an equivalent to C++ variadic parameter lists.
Generic units are explained in section 12 of the Ada 2012 Language Reference Manual.
Generic parameters can be:
·         Formal objects
·         Formal private types and derived types
·         Formal scalar types
·         Formal array types
·         Formal access types
·         Formal subprograms
·         Formal packages
Example of a generic package:
generic
   Size : Positive;
   type Item is private;
package Stack is
   procedure Push(E : in  Item);
   procedure Pop (E : out Item);
   Overflow, Underflow : exception;
end Stack;

package body Stack is

   type Table is array (Positive range <>) of Item;
   Space : Table(1 .. Size);
   Index : Natural := 0;

   procedure Push(E : in Item) is
   begin
      if Index >= Size then
         raise Overflow;
      end if;
      Index := Index + 1;
      Space(Index) := E;
   end Push;

   procedure Pop(E : out Item) is
   begin
      if Index = 0 then
         raise Underflow;
      end if;
      E := Space(Index);
      Index := Index - 1;
   end Pop;

end Stack;
This example implements a generic stack. The package specification begins with  the reserved word “generic” followed by the list of generic parameters. In this case the parameter Size is a generic formal object of the subtype Positive, and the type Item is any non-limited type, private or public.
The package specification only contains two procedures, Push and Pop, plus the definition of two exceptions, Overflow and Underflow. This package implements a singleton stack, therefore no stack type is publicly exposed.
The package body contains the implementation of the Push and Pop procedures plus the definition of the singleton stack object.

14.2.                 Template instantiation and specialization

14.2.1.                        Declare template specializations in the same file as the primary template they specialize

Partial and explicit specializations of function and class templates should be declared with the primary template.
This will ensure that implicit specializations will only occur when there is no explicit declaration available.

Corresponding Ada rule:

Ada generics do not undergo specialization, only instantiation. Examples of instantiation of the generic singleton stack package shown above are:
Package Stack_Int is new Stack(Size => 200, Item => Integer);

Package Stack_Bool is new Stack(100, Boolean);

Note that the generic parameters can be passed by name or by position. After the instantiations above the package procedures can be called as follows:
Stack_Int.Push(N);

Stack_Bool.Push(True);

Note that Stack_Int is a singleton stack containing Integer values while Stack_Bool is a distinct and separate singleton stack containing Boolean values.

14.2.2.                        Do not explicitly specialize a function template that is overloaded with other templates

Overload resolution does not take into account explicit specializations of function templates. Only after overload resolution has chosen a function template will any explicit specializations be considered.

Corresponding Ada rule:

Ada generic formal parameters are much richer and more explicit than C++ template parameters rendering specialization unnecessary.
Use of a formal package parameter in a generic parameter list does not necessarily cause overloading of subprograms.

14.2.3.                        Declare extern an explicitly instantiated template

Declaring the template with extern will disable implicit instantiation of the template when it is used in other translation units, saving time and reducing compile time dependencies.

Corresponding Ada rule:

Ada generics are never implicitly instantiated.

15.           Exception handling

15.1.                 Throwing an exception

15.1.1.                        Only use instances of std::exception for exceptions

Exceptions pass information up the call stack to a point where error handling can be performed. If an object of class type is thrown, the class type itself serves to document the cause of an exception.
Only types that inherit from std::exception, should be thrown.

Corresponding Ada rule:

Only instances of the pre-defined type Exception can be raised. Each instance of exception that is raised can be accompanied by exception information in the form of a string.

15.2.                 Constructors and destructors

15.2.1.                        Do not throw an exception from a destructor

The 2011 C++ Language Standard states that unless a user provided destructor has an explicit exception specification, one will be added implicitly, matching the one that an implicit destructor for the type would have received.
Furthermore when an exception is thrown, stack unwinding will call the destructors of all objects with automatic storage duration still in scope up to the location where the exception is eventually caught.
The program will immediately terminate should another exception be thrown from a destructor of one of these objects.

Corresponding Ada rule:

Ada has no explicit constructors or destructors. Instead, the procedures Initialize and Finalize are called. When a programmer customizes Initialize and Finalize care must be taken not to raise exceptions.

15.3.                 Handling an exception

15.3.1.                        Do not access non-static members from a catch handler of constructor/destructor function try block

When a constructor or a destructor has a function try block, accessing a non-static member from an associated exception handler will result in undefined behavior.

Corresponding Ada rule:

The response to one or more exceptions is specified by an exception_handler.

Syntax

handled_sequence_of_statements ::= 
     
sequence_of_statements
  [exception
     
exception_handler
    {
exception_handler}]

exception_handler ::= 
  when [
choice_parameter_specification:] exception_choice {| exception_choice} =>
     
sequence_of_statements

choice_parameter_specification ::= defining_identifier

exception_choice ::= exception_name | others

Example of an exception handler: 

begin
   Open(File, In_File, "input.txt"); 
exception
   when E : Name_Error =>
      Put("Cannot open input file : ");
      Put_Line(Exception_Message(E));
      raise;
end;

15.3.2.                        Ensure that a program does not result in a call to std::terminate

The path of an exception should be logical and well defined. Throwing an exception that is never subsequently caught, or attempting to rethrow when an exception is not being handled is an indicator of a problem with the design.

Corresponding Ada rule:

Ada exceptions should be handled wherever possible. It is considered bad design to use exceptions as a normal path to program termination.

16.           Preprocessing

16.1.                 Source file inclusion

16.1.1.                        Use the preprocessor only for implementing include guards, and including header files with include guards

The preprocessor should only be used for including header files into other headers or the main source file, in order to form a translation unit. In particular only the following include directive forms should be used:
·         #include <xyz>

·         #include "xyz"

Corresponding Ada rule:

Ada does not require the use of a preprocessor. File dependencies are established through the use of a dependency clause (with clause).
Dependency clauses do not require any equivalent of include guards. Dependency clauses cannot create macros.

16.1.2.                        Do not include a path specifier in filenames supplied in #include directives

Hardcoding the path to a header file in a #include directive may necessitate changes to source code when it is reused or ported.
Alternatively, the directory containing the header file should be passed to the compiler on command line (e.g. –I or /i option).

Corresponding Ada rule:

Ada dependency clauses do not contain filenames. They contain the names of compilation units.

16.1.3.                        Match the filename in a #include directive to the one on the filesystem

Some operating systems have case insensitive filesystems. Code initially developed on such a system may not compile successfully when ported to a case sensitive filesystem.

Corresponding Ada rule:

Ada dependency clauses do not contain filenames. They contain the names of compilation units.

16.1.4.                        Use <> brackets for system and standard library headers. Use quotes for all other headers.

It is common practice that #include <...> is used for compiler provided headers, and #include "..." for user provided files.
Adhering to this guideline therefore helps with the understandability and maintainability of the code.

Corresponding Ada rule:

Ada dependency clauses do not contain filenames. They contain the names of compilation units.

16.1.5.                        Include directly the minimum number of headers required for compilation

Presence of spurious include directives can considerably slow down compilation of a large code base. When a source file is refactored, the list of included headers should be reviewed, to remove include directives which are no longer needed.
Doing so may also offer an opportunity to delete from code repository source files that are no longer used in the project, therefore reducing the level of technical debt.

Corresponding Ada rule:

It is a good practice to minimize the dependency list for an Ada compilation unit. Minimized dependencies support code readability and maintenance.

17.           Standard library

17.1.                 General

17.1.1.                        Do not use std::vector<bool>

The std::vector<bool> specialization does not conform to the requirements of a container and does not work as expected in all STL algorithms.
In particular &v[0] does not return a contiguous array of elements as it does for other vector types. Additionally, the C++ Language Standard guarantees that different elements of an STL container can safely be modified concurrently, except for a container of std::vector<bool> type.

Corresponding Ada rule:

There is no corresponding prohibition for Ada standard libraries.

17.2.                 The C standard library

17.2.1.                        Wrap use of the C Standard Library

The C11 standard library, which is included in the C++ standard library, leaves the handling of concerns relating to security and concurrency up to the developer.
Therefore, if the C standard library is to be used, it should be wrapped, with the wrappers ensuring that undefined behavior and data races will not occur.

Corresponding Ada rule:

The C standard library is not included in the Ada 2012 standard library.

17.3.                 General utilities library

17.3.1.                        Do not use std::move on objects declared with const or const & type

An object with const or const & type will never actually be moved as a result of calling std::move.

Corresponding Ada rule:

There is no operation equivalent to C++ std::move defined in the Ada 2012 standard.

17.3.2.                        Use std::forward to forward universal references

The std::forward function takes the value category of universal reference parameters into account when passing arguments through to callees.
When passing a non universal reference argument std::move should be used.
Note: As auto is implemented with argument deduction rules, an object declared with auto && is also a universal reference for the purposes of this rule.

Corresponding Ada rule:

There are no Ada library components needed to pass parameter values to subprograms.

17.3.3.                        Do not subsequently use the argument to std::forward

Depending on the value category of arguments used in the call of the function, std::forward may or may not result in a move of the parameter.
When the value category of the parameter is an lvalue, then modifications to the parameter will affect the argument of the caller. In the case of an rvalue, the value should be considered as being indeterminate after the call to std::forward (See Rule 8.4.1: ”Do not access an invalid object or an object with indeterminate value”).

Corresponding Ada rule:

There are no Ada library components needed to pass parameter values to subprograms.

17.3.4.                        Do not create smart pointers of an array type

Memory allocated with array new must be deallocated with array delete. A smart pointer that refers to an array object must have this information passed in when the object is created. A consequence of this is that it is not possible to construct such a smart pointer using std::make shared.
A std::array or std::vector can be used in place of the raw array type. The usage and performance will be very similar but will not have the additional complexity required when deallocating the array object.

Corresponding Ada rule:

There is not special complexity dealing with allocating or deallocating arrays in Ada.

17.3.5.                        Do not create an rvalue reference of std::array

The std::array class is a wrapper for a C style array. The cost of moving std::array is linear with each element of the array being moved. In most cases, passing the array by & or const & will provide the required semantics without this cost.

Corresponding Ada rule:

There is no standard wrapper for Ada arrays. The cost of assigning an array or an array slice is equivalent to a memory copy in Ada.

17.4.                 Containers library

17.4.1.                        Use const container calls when result is immediately converted to a const iterator

The 2011 C++ Language Standard introduced named accessors for returning const iterators. Using these members removes an implicit conversion from iterator to const iterator.
Another benefit is that the declaration of the iterator object can then be changed to use auto without the danger of affecting program semantics.

Corresponding Ada rule:

Ada container packages define cursors to traverse a container objects. The cursor type is a private type defined in each standard container package.

17.4.2.                        Use API calls that construct objects in place

The 2011 C++ Language Standard allows for perfect forwarding. This allows for the arguments to a constructor to be passed through an API and therefore allowing for the final object to be constructed directly where it is intended to be used.

Corresponding Ada rule:

Parameter passing needs no special passing helpers.

17.5.                 Algorithms library

17.5.1.                        Do not ignore the result of std::remove, std::remove_if or std::unique

The mutating algorithms std::remove, std::remove if and both overloads of std::unique operate by swapping or moving elements of the range they are operating over.
On completion, they return an iterator to the last valid element. In the majority of cases the correct behavior is to use this result as the first operand in a call to std::erase.

Corresponding Ada rule:

While Ada does not provide an equivalent to these functions as separate library components, the concept of ignoring the return value of a function is foreign to Ada. All function return values must be used as an rvalue to some expression.

18.           Concurrency

18.1.                 General

18.1.1.                        Do not use platform specific multi-threading facilities

Rather than using platform-specific facilities, the C++ standard library should be used as it is platform independent.

Corresponding Ada rule:

Ada tasking is platform independent. There are no platform specific multi-threading facilities written with an Ada API.

18.2.                 Threads

18.2.1.                        Use high_intergrity::thread in place of std::thread

The destructor of std::thread will call std::terminate if the thread owned by the class is still joinable. By using a wrapper class a default behavior can be provided.

Corresponding Ada rule:

Ada tasks do not have an explicit join command nor an explicit detach command.
Ada tasks do have a dependency hierarchy. Each task (other than the environment task) depends on one or more masters. A task is said to be completed when the execution of the corresponding task body is completed. A task is said to be terminated when any finalization of the task body has been performed. The first step in finalizing a master is to wait for the termination of any tasks dependent upon the master. The task executing the master is blocked until all dependents have terminated. Any remaining finalization is then performed and the master is left.

18.2.2.                        Synchronize access to data shared between threads using a single lock

Using the same lock when accessing shared data makes it easier to verify the absence of problematic race conditions.
To help achieve this goal, access to data should be encapsulated such that it is not possible to read or write to the variable without acquiring the appropriate lock. This will also help limit the amount of code executed in the scope of the lock.
Note: Data may be referenced by more than one variable, therefore this requirement applies to the complete set of variables that could refer to the data.
Special attention needs to be made for const objects. The standard library expects operations on const objects to be thread-safe. Failing to ensure that this expectation is fulfilled may lead to problematic data races and undefined behavior. Therefore, operations on const objects of user defined types should consist of either reads entirely or internally synchronized writes.

Corresponding Ada rule:

Ada provides two forms of synchronization for passing data between tasks.
The Rendezvous mechanism provides a means to synchronously pass data directly between two tasks.
The Protected Object provides a way to pass data between tasks through a shared buffer. Protected objects are allowed to have a combination of three kinds of methods.
Protected procedures allow data in the protected object to be modified or updated unconditionally. Protected procedures implicitly manipulate a read/write lock on the protected object. Protected entries allow data in the protected object to be modified or updated conditionally.
 Protected entries have a boundary condition which must be satisfied. When the boundary condition evaluates to False the protected entry is blocked and the calling task is suspended and placed in an entry queue. Protected entries automatically manipulate a read/write lock on the protected object. Protected functions are only allowed read access to the protected object. Protected functions may not modify or update the state of the protected object. Protected functions automatically manipulate a shared read lock on the protected object allowing multiple tasks to read from the protected object simultaneously.

18.2.3.                        Do not share volatile data between threads

Declaring a variable with the volatile keyword does not provide any of the required synchronization guarantees:
·         Atomicity
·         Visibility
·          Ordering
Use mutex locks or ordered atomic variables, to safely communicate between threads and to prevent the compiler from optimizing the code incorrectly.

Corresponding Ada rule:

Use the Rendevous or protected objects to communicate between tasks.

18.2.4.                        Use the std::call_once rather than the Double_Checked Locking pattern

The Double-Checked Locking pattern can be used to correctly synchronize initializations.
However, the C++ standard library provides std::call_once which allow for a cleaner implementation.
Initialization of a local object with static storage duration is guaranteed by the C++ Language Standard to be reentrant.
However this conflicts with Rule 3.3.1: ”Do not use variables with static storage duration”, which takes precedence.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.3.                 Mutual exclusion

18.3.1.                        Within the scope of a lock, ensure that no static path results in a lock of the same mutex

It is undefined behavior if a thread tries to lock a std::mutex it already owns, this should therefore be avoided.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.3.2.                        Ensure that order of nesting of locks in a project forms a DAG

Mutex locks are a common causes of deadlocks. Multiple threads trying to acquire the same lock but in a different order may end up blocking each other.
When each lock operation is treated as a vertex, two consecutive vertices with no intervening lock operation in the source code are considered to be connected by a directed edge. The resulting graph should have no cycles, i.e. it should be a Directed Acyclic Graph (DAG).

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.3.3.                        Do not use stfd::recursive_mutex

Use of std::recursive mutex is indicative of bad design: Some functionality is expecting the state to be consistent which may not be a correct assumption since the mutex protecting a resource is already locked.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.3.4.                        Only use std::unique_lock when std::lock_guard cannot be used

The std::unique lock type provides additional features not available in std::lock guard. There is an additional cost when using std::unique lock and so it should only be used if the additional functionality is required.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.3.5.                        Do not use the members of std::mutex directly

A mutex object should only be managed by the std::lock guard or std::unique lock object that owns it.

18.3.6.                        Do not use relaxed atomics

Using non-sequentially consistent memory ordering for atomics allows the CPU to reorder memory operations resulting in a lack of total ordering of events across threads. This makes it extremely difficult to reason about the correctness of the code.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.

18.4.                 Condition variables

18.4.1.                        Do not use std::condition_variable_any on a std::mutex

When using std::condition variable any, there is potential for additional costs in terms of size, performance or operating system resources, because it is more general than std::condition variable.
std::condition variable works with std::unique lock<std::mutex>, while std::condition variable any can operate on any objects that have lock and unlock member functions.

Corresponding Ada rule:

Allow the Ada tasking mechanisms to perform implicit lock manipulations.


  • 22 March 2017 at 20:31

Ada vs C++ Bit-fields

Uses of Bit-fields
Bit-fields are typically used to implement communication protocols and to create device drivers for actuators or sensors. Frequently the actuators or sensors will use A/D and D/A converters to associate specific bit groupings with analog voltages on device connector pins.
In safety critical systems the bit patterns of the bit-fields must be correctly arranged or communication with the actuator or sensor will be incorrect. Since many safety critical systems require command of actuators, combined with resulting sensor readings to provide a closed loop control system, the ability to correctly define the bit layout for each device interface is highly safety critical.

JSF C++ Coding Standard

AV Rule 154 (MISRA Rules 111 and 112, Revised)

Bit-fields shall have explicitly unsigned integral or enumeration types only.



Rationale: Whether a plain (neither explicitly signed nor unsigned) char, short, int or long bit-field is signed or unsigned is implementation-defined.[10] Thus, explicitly declaring a bit-filed unsigned prevents unexpected sign extension or overflow.
Note: MISRA Rule 112 no longer applies since it discusses a two-bit minimum-length requirement for bit-fields of signed types.

AV Rule 155

Bit-fields will not be used to pack data into a word for the sole purpose of saving space.
Note: Bit-packing should be reserved for use in interfacing to hardware or conformance to communication protocols.
Warning: Certain aspects of bit-field manipulation are implementation-defined.


Rationale: Bit-packing adds additional complexity to the source code. Moreover, bit-packing may not save any space at all since the reduction in data size achieved through packing is often offset by the increase in the number of instructions required to pack/unpack the data.

AV Rule 156  (MISRA Rule 113)

All the members of a structure (or class) shall be named and shall only be accessed via their names.

Rationale: Reading/writing to unnamed locations in memory is error prone.
Exception: An unnamed bit-field of width zero may be used to specify alignment of the next bit-field at an allocation boundary. [10], 9.6(2)



C++ Bit-fields Explained

The following explanation of C++ bit-fields is taken from http://en.cppreference.com/w/cpp/language/bit_field

Bit field

Declares a class data member with explicit size, in bits. Adjacent bit field members may be packed to share and straddle the individual bytes.
A bit field declaration is a class data member declaration which uses the following declarator:
identifier(optional)attr(optional) : size
(1)
The type of the bit field is introduced by the decl-specifier-seq of the declaration syntax.
attr(C++11)
-
optional sequence of any number of attributes
identifier
-
the name of the bit field that is being declared. The name is optional: nameless bitfields introduce the specified number of bits of padding
size
-
an integral constant expression with a value greater or equal to zero. When greater than zero, this is the number of bits that this bit field will occupy. The value zero is only allowed for nameless bitfields and has special meaning: it specifies that the next bit field in the class definition will begin at an allocation unit's boundary.

Explanation

The number of bits in a bit field sets the limit to the range of values it can hold:
#include <iostream>
struct S {
// three-bit unsigned field,
// allowed values are 0...7

};
int main()
{
S s
s.b
std::cout << s.b << '\n'; // output: 0
}
Multiple adjacent bit fields are usually packed together (although this behavior is implementation-defined):
#include <iostream>
struct S {
// will usually occupy 2 bytes:
// 3 bits: value of b1
// 2 bits: unused
// 6 bits: value of b2
// 2 bits: value of b3
// 3 bits: unused

};
int main()
{
std::cout << sizeof(S) << '\n'; // usually prints 2
}
The special unnamed bit field of size zero can be forced to break up padding. It specifies that the next bit field begins at the beginning of its allocation unit:
#include <iostream>
struct S {
// will usually occupy 2 bytes:
// 3 bits: value of b1
// 5 bits: unused
// 6 bits: value of b2
// 2 bits: value of b3




};
int main()
{
std::cout << sizeof(S) << '\n'; // usually prints 2
}
If the specified size of the bit field is greater than the size of its type, the value is limited by the type: a std::uint8_t b : 1000; would still hold values between 0 and 255. the extra bits become unused padding.
Because bit fields do not necessarily begin at the beginning of a byte, address of a bit field cannot be taken. Pointers and non-const references to bit fields are not possible. When initializing a const reference from a bit field, a temporary is created (its type is the type of the bit field), copy initialized with the value of the bit field, and the reference is bound to that temporary.
The type of a bit field can only be integral or enumeration type.
A bit field cannot be a static data member.

Notes

The following properties of bit fields are implementation-defined
  • Everything about the actual allocation details of bit fields within the class object
  • For example, on some platforms, bit fields don't straddle bytes, on others they do
  • Also, on some platforms, bit fields are packed left-to-right, on others right-to-left
  • Whether char, short, int, long, and long long bit fields that aren't explicitly signed or unsigned are signed or unsigned.
  • For example, int b:3; may have the range of values 0..7 or -4..3.
(until C++14)

References

  • C++11 standard (ISO/IEC 14882:2011):
  • 9.6 Bit-fields [class.bit]
  • C++98 standard (ISO/IEC 14882:1998):
  • 9.6 Bit-fields [class.bit]


Ada Alternative

Ada allows the programmer to define bit layouts within a record with much more confidence than can be done by the C++ programmer. The Ada 2012 Reference Manual describes the use of record representation clauses.

13.5.1 Record Representation Clauses

1
record_representation_clause specifies the storage representation of records and record extensions, that is, the order, position, and size of components (including discriminants, if any).

Syntax

2
record_representation_clause ::= 
    
for first_subtype_local_name use
      
record [mod_clause]
        {
component_clause}
      
end record;
3
component_clause ::= 
    
component_local_name at position range first_bit .. last_bit;
4
position ::= static_expression
5
first_bit ::= static_simple_expression
6
last_bit ::= static_simple_expression

Name Resolution Rules

7
Each positionfirst_bit, and last_bit is expected to be of any integer type. 

Legality Rules

8/2
The first_subtype_local_name of a record_representation_clause shall denote a specific record or record extension subtype. 
9
If the component_local_name is a direct_name, the local_name shall denote a component of the type. For a record extension, the component shall not be inherited, and shall not be a discriminant that corresponds to a discriminant of the parent type. If the component_local_name has an attribute_designator, the direct_name of the local_name shall denote either the declaration of the type or a component of the type, and theattribute_designator shall denote an implementation-defined implicit component of the type.
10
The positionfirst_bit, and last_bit shall be static expressions. The value of position and first_bit shall be nonnegative. The value of last_bit shall be no less than first_bit – 1. 
10.1/2
   If the nondefault bit ordering applies to the type, then either: 
10.2/2
the value of last_bit shall be less than the size of the largest machine scalar; or
10.3/2
the value of first_bit shall be zero and the value of last_bit + 1 shall be a multiple of System.Storage_Unit. 
11
At most one component_clause is allowed for each component of the type, including for each discriminant (component_clauses may be given for some, all, or none of the components). Storage places within acomponent_list shall not overlap, unless they are for components in distinct variants of the same variant_part.
12
A name that denotes a component of a type is not allowed within a record_representation_clause for the type, except as the component_local_name of a component_clause.

Static Semantics

13/2
 record_representation_clause (without the mod_clause) specifies the layout.
13.1/2
   If the default bit ordering applies to the type, the positionfirst_bit, and last_bit of each component_clause directly specify the position and size of the corresponding component.
13.2/3
   If the nondefault bit ordering applies to the type, then the layout is determined as follows:
13.3/2
the component_clauses for which the value of last_bit is greater than or equal to the size of the largest machine scalar directly specify the position and size of the corresponding component;
13.4/2
for other component_clauses, all of the components having the same value of position are considered to be part of a single machine scalar, located at that position; this machine scalar has a size which is the smallest machine scalar size larger than the largest last_bit for all component_clauses at that position; the first_bit and last_bit of each component_clause are then interpreted as bit offsets in this machine scalar. 
14
record_representation_clause for a record extension does not override the layout of the parent part; if the layout was specified for the parent type, it is inherited by the record extension. 

Implementation Permissions

15
An implementation may generate implementation-defined components (for example, one containing the offset of another component). An implementation may generate names that denote such implementation-defined components; such names shall be implementation-defined attribute_references. An implementation may allow such implementation-defined names to be used in record_representation_clauses. An implementation can restrict such component_clauses in any manner it sees fit. 
16
If a record_representation_clause is given for an untagged derived type, the storage place attributes for all of the components of the derived type may differ from those of the corresponding components of the parent type, even for components whose storage place is not specified explicitly in the record_representation_clause.

Implementation Advice

17
The recommended level of support for record_representation_clauses is: 
17.1/2
An implementation should support machine scalars that correspond to all of the integer, floating point, and address formats supported by the machine.
18
An implementation should support storage places that can be extracted with a load, mask, shift sequence of machine code, and set with a load, shift, mask, store sequence, given the available machine instructions and run-time model.
19
A storage place should be supported if its size is equal to the Size of the component subtype, and it starts and ends on a boundary that obeys the Alignment of the component subtype.
20/2
For a component with a subtype whose Size is less than the word size, any storage place that does not cross an aligned word boundary should be supported.
21
An implementation may reserve a storage place for the tag field of a tagged type, and disallow other components from overlapping that place. 
22
An implementation need not support a component_clause for a component of an extension part if the storage place is not after the storage places of all components of the parent type, whether or not those storage places had been specified. 
NOTES
23
14  If no component_clause is given for a component, then the choice of the storage place for the component is left to the implementation. If component_clauses are given for all components, the record_representation_clause completely specifies the representation of the type and will be obeyed exactly by the implementation. 

Examples

24
Example of specifying the layout of a record type: 
25
Word : constant := 4;  --  storage element is byte, 4 bytes per word
26
type State         is (A,M,W,P);
type Mode          is (Fix, Dec, Exp, Signif);
27
type Byte_Mask     is array (0..7)  of Boolean;
type State_Mask    is array (State) of Boolean;
type Mode_Mask     is array (Mode)  of Boolean;
28
type Program_Status_Word is
  record
      System_Mask        : Byte_Mask;
      Protection_Key     : Integer range 0 .. 3;
      Machine_State      : State_Mask;
      Interrupt_Cause    : Interruption_Code;
      Ilc                : Integer range 0 .. 3;
      Cc                 : Integer range 0 .. 3;
      Program_Mask       : Mode_Mask;
      Inst_Address       : Address;
end record;
29
for Program_Status_Word use
  record
      System_Mask      at 0*Word range 0  .. 7;
      Protection_Key   at 0*Word range 10 .. 11; -- bits 8,9 unused
      Machine_State    at 0*Word range 12 .. 15;
      Interrupt_Cause  at 0*Word range 16 .. 31;
      Ilc              at 1*Word range 0  .. 1;  -- second word
      Cc               at 1*Word range 2  .. 3;
      Program_Mask     at 1*Word range 4  .. 7;
      Inst_Address     at 1*Word range 8  .. 31;
  end record;
30
for Program_Status_Word'Size use 8*System.Storage_Unit;
for Program_Status_Word'Alignment use 8;
NOTES
31
15  Note on the example: The record_representation_clause defines the record layout. The Size clause guarantees that (at least) eight storage elements are used for objects of the type. The Alignment clause guarantees that aliased, imported, or exported objects of the type will have addresses divisible by eight.

Notice that Ada allows not only the number of bits for each field, it also allows the programmer to specify the value range for each field, the bit location relative to the start of each word of the structure, and even leave some unused bits. C++ requires the use of unnamed bit-fields (with a size greater than 0) to mark unused bits. The C++ compiler has no way to know which bits the programmer wants to skip without specifying an unnamed field. Strangely, an unnamed field with a size of 0 indicates that the next field should start at an allocation unit's boundary, which is highly dependent on the hardware architecture. For instance, on an 8-bit processor the allocation boundary will be every 8 bits, while on a 32-bit processor the allocation boundary might be every 8 bits, 16 bits, or 32 bits, depending upon the processor architecture.

As a software safety engineer I find the implementation-defined aspects of C++ bit-fields to be very unsettling. A project may verify that its C++ bit-fields work as intended on a particular architecture, but when a technology refresh is performed in several years, there is no assurance that the C++ program will continue to work properly on the new hardware. The failure of the C++ program to run on the new hardware will be received as a complete surprise by most users of the upgraded system and by their management. The surprise may be accompanied by hazardous events because of incorrect control of a safety critical system. People may be injured or die, the environment may be seriously damaged, and very expensive systems may be damaged or destroyed. The root cause of the hazards will not be poor functional requirements, poor design, or programming mistakes. The root cause of the hazards will be exposure of implementation-defined behaviors in a programming language. Who will the lawyers sue over the consequences of those hazards?
  • 22 March 2014 at 00:45
❌
❌