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:
- memory allocation (
new
) - 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:
- You have to remember to
delete
.Technically, you an use a smart pointer variable to take care of that for you in modern C++, but you should definitely not heap-allocate unless you have to.
- Pointers are not expected by the framework.
You always have to remember to dereference the pointer to use it with the rest of the framework. In a worst case, C++ might find a conversion operation to make something totally unexpected happen if you forget to dereference the pointer.
- It's easy to confuse yourself.
You compare the pointer to
NULL
and think you have verified that you have a valid Java object, when all you've done is verify that you have a valid C++ pointer to a proxy object that could refer to Javanull
. You would have to write something likep != NULL && *p != null
to check for Javanull
.
Take-Away Points
- 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.
_use_java_ctor
Handles the Special CasesSimply add
_use_java_ctor
to state your intent of invoking the Java constructor.- 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.