Emulating Pointer-To-Member

I recently came across a situation where it would be convenient to have a pointer-to-member (a la C++). Ada doesn’t have this feature natively, so I’m trying to see how close I can come to emulating it.

For those not familiar with this concept, I’ll explain briefly. Normally an access type points to a single value (which could be a member of a record or not). For any given pointer value, the value it points to remains constant, because what the access type holds is an absolute memory address. A pointer-to-member, in contrast, points to a specific member of any record. For any given pointer-to-member value, the actual value it points to changes based on which record instance it is paired with. Instead of an absolute address, it holds an offset from the start of a record.

Here’s my attempt to recreate that in Ada.

The pointer-to-member type itself is pretty simple:

type Ptr_Type is new System.Storage_Elements.Storage_Offset;

A function that dereferences this type has to do some magic internally:

procedure Set (Base : in out T; Ptr : Ptr_Type; Value : Integer) is
   use System.Storage_Elements;
   package Cast is new System.Address_To_Access_Conversions (Integer);
begin
   Cast.To_Pointer (Base'Address + Ptr).all := Value;
end Set;

Creating a pointer-to-member is unfortunately awkward. Ada provides the ’Position attribute, which is nice, but it requires an object. The best way I’ve found to do this is to create a temporary:

Ptr : Ptr_Type := T'(others => <>).Some_Member'Position;

Unfortunately, this is all very hacky. Type safety is thrown out the window – there’s nothing requiring Some_Member is an Integer or that it’s a member of T. I’m not sure what this will do to the compiler’s alias analysis or what happens with tagged types and OOP. Wrapping some of it with utility functions and a generic package will help, but creating the pointer will always be an issue as far as I can tell.

How else have you solved this particular issue? Feedback is welcome.

What problem are you trying to solve with this technique?

2 Likes

I’m going to use other techniques to solve the immediate issue, I think. For now, this is more an exercise in what’s possible rather than to solve any specific problem.

First of all it is a not pointer it is an offset. A pointer (so-called fat pointer) would contain an access to the object and the offset.

I use offsets to members (introspection, actually) in these cases:

  • Persistency when an object or its parts are stored in data base blobs;
  • Network protocols to describe a packet payload;
  • Process communication, when an object is located in shared memory, e.g. a file map.

For implementation I use fake streams. The idea is that 'Write attribute of a stream enumerates record members so that I can get members through “writing” it into a stream. This technique is used in the Simple Components. Note that it requires a base type. For example all process communication objects (members of the record type mapped into shared memory) are derived from Abstract_Shared_Object.

Here is a small article on the subject.

Nothing bad, because Ada does not have full multiple inheritance.

“Pointer-to-member” is the terminology used elsewhere. It’s certainly not a pointer in the same way access types generally are. Though what is a pointer except an offset in memory?

Philosophical questions aside … the goal here is to create a safe abstraction around that offset. The “fake stream” idea is interesting and definitely solves an overlapping problem set.

An alternative I thought of over the weekend is to use function pointers instead:

function Get_Some_Member (This : access T) return access Integer
is (This.Some_Member'Access);

Passing around a pointer to accessor functions like this emulates the functionality in a type-safe manner. I wouldn’t be surprised if inlining allows this to compile to the same assembly. The amount of boilerplate is unfortunate, though.

Pointer allows a direct access. It is not necessary an offset to the address 0, older machines had strange memory architectures. Offset is an offset to some base. It was introduced first in PL/1, AFAIK. The difference is that you can move the object around keeping the offset.

That is like a getter/setter pair.

Are you talking about the following operators in C++ or something else?

.*
->*

It isn’t something that is commonly used. You won’t see a lot of this in C++ coding as very few people know it exists. In 40 years of using C++, I’ve only used it twice and not seen any internet solutions or code at work that use it except when it is example code of how to use it. Most C++ books only have about 10 lines or at most half a page on it and quickly move on to the next topic. Many just describe it but do not give a proper example of usage. You might try translating the following to check whether you solution works.

#include <iostream>
int main()
{
    class Octopus
    {
    public:
        int leg1;
        int leg2;
    };
    Octopus eight { 10, 20};
    Octopus *nine = new Octopus(eight);
    // Declare the member pointer
    int (Octopus::*squid);

    // Use with .* operator
    squid = &Octopus::leg2;
    std::cout << ".*  " << eight.*squid << std::endl;

    // Use with ->* operator
    squid = &Octopus::leg1;
    std::cout << "->* " << nine->*squid << std::endl;
    return 0;

}

The result is

.*  20
->* 10

Here’s something close to the octopus example:

package Octopus_Types is
   type Octopus_Type is tagged record
      leg1: aliased Integer;
      leg2: aliased Integer;
   end record with
      Constant_Indexing => const_ref;

   type leg_ptr_t is private;
   leg1_ptr : constant leg_ptr_t;
   leg2_ptr : constant leg_ptr_t;

   type const_ref_t (leg : not null access constant Integer) is private with
      Implicit_Dereference => leg;

   function const_ref (
      Octopus : aliased Octopus_Type; 
      leg_ptr : leg_ptr_t) return const_ref_t;

private

   type leg_ptr_t is new Integer;

   leg1_ptr : constant leg_ptr_t := 1;
   leg2_ptr : constant leg_ptr_t := 2;
   
   type const_ref_t (leg : not null access constant Integer) is record
      null;
   end record;

end Octopus_Types;


package body Octopus_Types is

   function const_ref (
      Octopus : aliased Octopus_Type; 
      leg_ptr : leg_ptr_t) return const_ref_t is
   begin
      if leg_ptr = leg1_ptr then
         return const_ref_t'(leg => Octopus.leg1'Access);
      else
         return const_ref_t'(leg => Octopus.leg2'Access);
      end if;
   end const_ref;

end Octopus_Types;
   

with Octopus_Types;  use Octopus_Types;
with Ada.Text_IO; use Ada.Text_IO;

procedure Test_Octopus is
   eight : Octopus_Type := Octopus_Type'(leg1 => 10, leg2 => 20);
   nine : Octopus_Type := eight;

   squid : leg_ptr_t;

begin
   squid := leg2_ptr;
   Put_Line (Integer'Image (eight (squid)));
   squid := leg1_ptr;
   Put_Line (Integer'Image (nine (squid)));
end;


This prints:

 20
 10

as in the example.

I tried an implementation that uses some Chap 13 tricks but couldn’t get it to compile.

Anyway, it’s kinda-sorta close to the C++ example.

It’s interesting that even though the const_ref function specifies that the Octopus parameter is aliased, you’re not required to declare the octopus objects eight or nine as aliased. Note sure if that was always true or whether some of the rules were liberalized at some point.

1 Like

Note that there’s nothing special to do for pointer-to-member-function, you can just use a pointer-to-subprogram in the normal way:

  type Op_Access is access procedure (Octopus : Octopus_Type);
  Op : Op_Access := Regrow_Leg'Access;
begin
  Op (eight);
  Op := Move_Leg'Access;
  Op (nine);

Ignoring C++ and just considering what could it be. A member function is a primitive operation. So a “pointer-to-member-function” must be dispatching. A very different thing from normal access-to-subprogram.

Except that pointer-to-member-function is typically what you use when you don’t want polymorphic dispatching.

But then it is just an access-to-subprogram we already have.

Yes, which was my point.