: code tutorial (v3)

Lesson 2: Constructing Proxy Objects

Introduction

Let's look at a few lines of Java code that deal with type java.util.Hashtable.

// creates a hashtable using the default (unparametrized) constructor 
Hashtable       ht1 = new Hashtable();

// creates a hashtable with initial space for 15 entries
Hashtable       ht2 = new Hashtable( 15 );

// does not create an object; initializes object reference to null
Hashtable       ht3;

If you have a solid Java background, it is probably unnecessary to point out that the new keyword is used to signal that you are creating a new object instance by invoking the immediately following constructor. In Java, the two steps of object creation:

  1. memory allocation (new)
  2. object initialization (constructor)

are inseparably intertwined and you cannot do one without the other.

In C++ things are—as usual—much more complicated. C++ allows you to separate the memory management from the object initialization. Objects of a type may be located in different types of memory, for example in a dynamic heap, on a thread's stack, or even in process-global read-only memory.

You can also pre-allocate memory without initializing it and then, at a later point, invoke a so-called placement constructor to turn the bulk memory into an object. To sum it up: C++ offers you many more options for memory and object management.

One of the biggest consequences of this difference between Java and C++ is that you don't have to use the keyword new to signal that you're creating an object. You can create an object by simply invoking the constructor or even by just declaring a variable of the desired type. In C++, new indicates that you are trying to dynamically reserve memory from the program's heap. Let's take a look at a C++ snippet that looks identical to the Java snippet above and see what would happen. Problematic statements are shown in red.

// does NOT compile
Hashtable       ht1 = new Hashtable();

// does NOT compiles
Hashtable       ht2 = new Hashtable( 15 );

// creates C++ object; initializes Java object reference to null
Hashtable       ht3;

The first two statements cause compilation errors because you are trying to assign a pointer to Hashtable to a variable that is declared to be of type Hashtable. Those two types are not assignment compatible and consequently that's not allowed in C++.

The third statement is fine and does exactly what you want it to do: it declares a C++ proxy instance and initializes it to the Java null value. After all, you don't really want to create a Java object each time you declare a variable of a proxy type! We will expand on this at greater detail momentarily.

So let's try to fix the compilation errors by declaring the variables as pointers.

// compiles but does not work as expected
Hashtable *     ht1 = new Hashtable();

// compiles and works as expected, but not what we should be doing
Hashtable *     ht2 = new Hashtable( 15 );

// creates C++ object; initializes Java object reference to null
Hashtable       ht3;

We got rid of our compilation errors, so what's the problem? There are actually multiple problems.

The first problem is that we now have heap-allocated C++ objects. We created them with new, which means that we have to destroy them explicitly with delete. If we forget to call delete we have a memory leak. This is particularly bad if we are trying to reproduce a Java code snippet in C++ because in Java the objects will be cleaned up automatically by the garbage collector and there is no explicit delete. So even though the second statement does what you expect it to do, we do not recommend creating proxy instances on the C++ heap unless you are entirely motivated to do so by your C++ requirements and not just by wanting to make the C++ code look like Java code. If you heap-allocate with new and forget to delete the instance you are definitely leaking a C++ object and probably also a Java object that now can't be garbage collected because there's still a C++ object that holds a reference to it.

The second problem is that the first statement does not do what you might think. This is where one of the unexpected pitfalls of C++ comes into play: the C++ default (unparametrized) constructor is special. Your C++ compiler invokes it automatically whenever you declare an instance of its type without providing any parenthesized arguments.

As motivated above, we have implemented the default constructor to simply initialize the proxy instance to refer to Java null. As you look at your C++ code, you think you have created a Java object using a no-argument Java constructor, but you really only invoked the C++ default constructor which set the Java reference to null.

The third problem is admittedly of our own making but it exists nevertheless and you should be aware of it if you use heap-allocated objects a lot. Our proxy framework passes all objects by reference. You would have to dereference every pointer variable to use it with the proxy types.

If you come from the C++ side, this might not be a surprise for you, but it might be a huge surprise to you if you come from the Java side. The following statements all do exactly the same thing:

Hashtable       ht1;
Hashtable       ht2();
Hashtable       ht3 = Hashtable();

All three create a C++ proxy object that is initialized to refer to null, i.e. no Java object. For a Java programmer that is very surprising, particularly the third statement.

The key is that they all invoke the C++ default constructor. As we pointed out above, the default constructor has to initialize the referenced Java object to null, otherwise you will inadvertently be creating a Java object each time you declare a variable of a proxy type.

"Normal" Constructors

This was all background explanation that does not apply to normal parametrized constructors. Parametrized Java constructors translate totally as expected to C++ and you can use them as you would expect. To create a Hashtable that's initialized with space for fifteen entries, you can simply write:

// invoke the corresponding Java constructor
Hashtable       ht1( 15 );

"Special" Constructors: _use_java_ctor

There are two special constructors: the unparametrized default constructor and the copy constructor. We're using their C++ names here but hopefully it's clear what they represent on the Java side: a no-arguments constructor and a constructor that takes one argument of its declaring type.

Both of these Java constructors would naturally translate into C++ constructors that have a special meaning in C++. The default constructor was explained above; the copy constructor is similar in that it is also invoked by the compiler during various C++ operations and should not be used explicitly.

To avoid the unintentional invocation of Java constructors we translate these two constructors differently. To invoke them, you have to explicitly make it clear that that is really what you want to do.

We introduced a marker type and a global variable named _use_java_ctor of that marker type. To invoke a no-arguments Java constructor, you simply write:

// invoke the Java no-arguments constructor
Hashtable       ht1( _use_java_ctor );

If you forget the _use_java_ctor argument you simply end up with a proxy instance that is initialized to null.

To invoke a constructor that takes an argument of its own declaring type as an argument, you write:

Hashtable       ht1( 15 );
// invoke the Java "copy" constructor
Hashtable       ht2( ht1, _use_java_ctor );

If you forget the _use_java_ctor argument you simply end up with a proxy instance that refers to the same object as its argument.

What about new?

We already explained above why it's probably a bad idea to use new to create proxy instances in C++. To sum it up:

Take-Away Points

  1. Most Constructors Translate as Expected.

    Only the no-arguments constructor and the "copy" constructor need special treatment that requires you to do something special to invoke them. For all other constructors you simply pass the arguments they require on the Java side.

  2. _use_java_ctor Handles the Special Cases

    Simply add _use_java_ctor to state your intent of invoking the Java constructor.

  3. Don't Use new Unless You Have To!

    The temptation is big when you reproduce a snippet of Java code into corresponding C++ code. Stay vigilant and don't be surprised by the compiler errors you will see if you miss one.