Gem #61: Interfacing with C++ constructors

by Javier MirandaArnaud Charlet —AdaCoreAdaCore

Let's get started…

Let's assume that we need to interface with the following C++ class:

class Root {
   int  a_value;
   int  b_value;
   virtual int Get_Value ();
   Root();              // Default constructor
   Root(int v);         // 1st non-default constructor
   Root(int v, int w);  // 2nd non-default constructor

Using the automatic binding generator, or writing manually, we can obtain the corresponding package spec:

with Interfaces.C; use Interfaces.C;
package Pkg_Root is

   type Root is tagged limited record
       A_Value : int;
       B_Value : int;
   end record;
   pragma Import (CPP, Root);

   function Get_Value (Obj : Root) return int;
   pragma Import (CPP, Get_Value);

   function New_Root return Root'Class;
   pragma Cpp_Constructor (New_Root, "_ZN4RootC1Ev");

   function New_Root (v : int) return Root'Class;
   pragma Cpp_Constructor (New_Root, "_ZN4RootC1Ei");

   function New_Root (v, w : int) return Root'Class;
   pragma Cpp_Constructor (New_Root, "_ZN4RootC1Eii");

end Pkg_Root;

On the Ada side, the constructor is represented by a function (whose name is arbitrary) that returns the class-wide type corresponding to the imported C++ class. The return type is required to be class-wide rather than the specific Root type so that the function will not be treated as a primitive dispatching operation of the type. Although the constructor is described as a function, it is typically a procedure with an extra implicit argument (the object being initialized) at the implementation level. GNAT issues the appropriate call, whatever it is, to initialize the object.

Constructors can appear in the following contexts:

- As the initialization expression of an object of type T
- As the initialization expression of a record component of type T
- In a limited aggregate
- In a limited aggregate used as the expression of a return statement

Note that all of the above contexts are places where Ada 2005 allows limited aggregates and calls to functions with limited results to appear, and in fact it's necessary to enable the Ada 2005 compilation mode (-gnat05) to make use of this feature.

In a declaration of an object whose type is a class imported from C++, either the default C++ constructor is implicitly called by GNAT, or else the required C++ constructor must be explicitly called in the expression that initializes the object. For example:

 Obj1 : Root;
 Obj2 : Root := New_Root;
 Obj3 : Root := New_Root (v => 10);
 Obj4 : Root := New_Root (30, 40);

The first two declarations are equivalent: in both cases, the default C++ constructor is invoked (in the former case the call to the constructor is implicit, and in the latter case the call is explicit in the object declaration). Obj3 is initialized by the C++ nondefault constructor that takes an integer argument, and Obj4 is initialized by the nondefault C++ constructor that takes two integers.

It's worth pointing out that normally it's not permitted to call a class-wide function to initialize an object of a specific type, and it's only in the case of these special imported constructor functions that the compiler allows this usage.

In the next Gem we will explore how to derive and extend imported C++ classes on the Ada side, and show uses of constructors in Ada 2005 extended return and limited aggregates.