C++ is a nice and very flexible language, but this comes at the cost that it forces you to think about many programming details before you can even think about solving your actual problem. Examples would be:
- object oriented or generic programming
- when to use references, value types and pointers
- memory management rules
- casting rules, const correctness, virtual methods
- STL? boost?
- …
This list can become quite endless. Being in the same boat as every other C++ programmer but having grasped some of the look&feel of other languages like Java and Objective-C (in its Cocoa incarnation) or even Qt (as a good C++ OO style example), I am continuously and unconsciously thinking about the pros and cons of each of them and recently I thought: wouldn’t it be nice to be able to program in C++ but make it look like Java?
Sure it would! Even it it were only just for fun…
So, I came up with a working code sample which looks quite like Java but is actually C++. I show you the code sample first before I elaborate on any details. First come some classes which you can consider the “framework”:
#include <iostream>
#include <sstream>
#include <assert.h>
#include <vector>
size_t globalRefCount = 0;
class Object;
class String;
std::ostream &operator<<(std::ostream&, Object&);
std::ostream &operator<<(std::ostream &, const String&);
class Object {
protected:
struct Impl {
size_t _refCount;
Impl() : _refCount(0) {}
virtual ~Impl() {}
} *data;
inline void retain() {
if(data) {
data->_refCount++;
globalRefCount++;
}
}
inline void release() {
if(data) {
data->_refCount--;
globalRefCount--;
if(data->_refCount == 0) {
delete data;
data = 0;
}
}
}
Object(): data(new Impl) {
retain();
}
Object(Impl *imp) : data(imp) {
retain();
}
Object(const Object& other): data(0) {
operator=(other);
}
public:
virtual ~Object() {
release();
}
virtual const char *type() const { return "Object"; }
void operator=(const Object& other) {
if(data!=other.data) {
release();
data = other.data;
retain();
}
}
// make a heap clone of this object for usage in containers
virtual Object *clone() const {
return new Object(*this);
}
virtual String toString() const;
};
class String : public Object {
protected:
struct Impl: public Object::Impl {
std::string str;
};
public:
String(): Object(new Impl) {}
String(const char *s): Object(new Impl) {
static_cast<Impl*>(data)->str = s;
}
String(const String &other): Object(other) {}
String operator+(const String& s) const {
String result;
static_cast<Impl*>(result.data)->str = static_cast<Impl*>(data)->str;
static_cast<Impl*>(result.data)->str += static_cast<Impl*>(s.data)->str;
return result;
}
String operator+(const Object& s) const {
String result;
static_cast<Impl*>(result.data)->str = static_cast<Impl*>(data)->str;
static_cast<Impl*>(result.data)->str += static_cast<Impl*>(s.toString().data)->str;
return result;
}
String operator+(long l) const {
std::ostringstream oss;
oss << static_cast<Impl*>(data)->str << l;
String result;
static_cast<Impl*>(result.data)->str = oss.str();
return result;
}
const char *c_str() const {
return static_cast<Impl*>(data)->str.c_str();
}
bool operator==(const String &other) const {
return static_cast<Impl*>(data)->str == static_cast<Impl*>(other.data)->str;
}
// must have's
const char *type() const { return "String"; }
Object *clone() const { return new String(*this); }
String toString() const {
return *this;
}
};
std::ostream &operator<<(std::ostream &os, const String& s) {
os << s.c_str();
return os;
}
String Object::toString() const {
std::ostringstream os;
os << this->type() << "@" << (void *)this << "[" << (data ? data->_refCount : 0) << "]";
return String(os.str().c_str());
}
std::ostream &operator<<(std::ostream &os, Object& o) {
os << o.toString().c_str();
return os;
}
class ClassCastException: public Object {
struct Impl: public Object::Impl {
String message;
};
public:
ClassCastException() : Object(new Impl) {}
ClassCastException(const String& msg) : Object(new Impl) {
static_cast<Impl*>(data)->message = msg;
}
const char *type() const { return "ClassCastException"; }
Object *clone() const { return new ClassCastException(*this); }
String message() const {
return static_cast<Impl*>(data)->message;
}
String toString() const {
return message();
}
};
class ArrayList: public Object {
struct Impl: public Object::Impl {
std::vector<Object*> _data;
};
public:
ArrayList(): Object(new Impl) {}
~ArrayList() {
Impl *self = static_cast<Impl*>(data);
for (std::vector<Object*>::iterator it = self->_data.begin(); it!=self->_data.end(); it++) {
delete *it;
}
}
void add(const Object& element) {
static_cast<Impl*>(data)->_data.push_back(element.clone());
}
size_t size() const {
return static_cast<Impl*>(data)->_data.size();
}
Object &at(size_t index) const {
return *static_cast<Impl*>(data)->_data.at(index);
}
template<class T> const T &at(size_t index) const {
Object *o = static_cast<Impl*>(data)->_data.at(index);
T *t = dynamic_cast<T*>(o);
if(t) return *t;
throw ClassCastException(o->type());
}
const char *type() const { return "ArrayList"; }
Object *clone() const { return new ArrayList(*this); }
String toString() const {
std::ostringstream oss;
oss << Object::toString() << "(";
for (size_t i = 0; i<size(); i++) {
oss << at(i).toString();
if(i+1<size()) {
oss << ",";
}
}
oss << ")";
return oss.str().c_str();
}
};
class OutputStream: public Object {
struct Impl: public Object::Impl {
std::ostream &stream;
Impl(std::ostream &os): stream(os) {}
};
OutputStream() {}
public:
OutputStream(std::ostream& os): Object(new Impl(os)) {}
OutputStream(const OutputStream &other): Object(other) {}
void println(const Object &object) {
static_cast<Impl*>(data)->stream << object.toString() << std::endl;
}
const char *type() const { return "OutputStream"; }
Object *clone() const { return new OutputStream(*this); }
};
Now let’s see how to use it in client code. I supply only a main() here but I have some anonymous blocks to show the effects of scoping:
struct system {
OutputStream out;
system(): out(std::cout) {}
};
struct system System;
int main (int argc, const char * argv[]) {
assert(globalRefCount==1); // 1 is for System.out
{
String s;
assert(globalRefCount==2);
}
assert(globalRefCount==1);
{
String s = "Connecting...";
System.out.println(String("s = ") + s);
String dots = "the dots";
String t = s + " " + dots + ".";
System.out.println(String("t = ") + t);
ArrayList l;
l.add(s);
assert(l.size() == 1);
assert(globalRefCount==6);
l.add(t);
l.add(ArrayList());
System.out.println(String("l = ") + l);
assert(globalRefCount==8);
try {
System.out.println(String("l[0] as String = ") + l.at<String>(0)); // ok
System.out.println(String("l[1] as String = ") + l.at<String>(1)); // ok
System.out.println(String("l[2] as String = ") + l.at<String>(2)); // this throws!
assert(false);
} catch(ClassCastException e) {
System.out.println(String("ClassCastException: ") + e);
l.add(e);
}
// adding to the list in other scope will keep the object valid
{
String other = "Created in other scope";
l.add(other);
}
System.out.println(String("l[3] as String = ") + l.at(3));
assert(l.size()==5);
assert(l.at<String>(4) == "Created in other scope");
System.out.println(String("l = ") + l);
}
assert(globalRefCount == 1);
return 0;
}
Pretty much like Java, isn’t it?
I have written this just as a proof of concept. As such, it is fully working and I like it so far. It could serve as a good starting point for a complete implementation. Here’s the output when executed:
s = Connecting...
t = Connecting... the dots.
l = ArrayList@0x7fff5fbff8c0[1](Connecting...,Connecting... the dots.,ArrayList@0x100100b00[1]())
l[0] as String = Connecting...
l[1] as String = Connecting... the dots.
l[2] as String = ClassCastException: ClassCastException@0x100100cb8[1]
l[3] as String = ClassCastException@0x100100b20[1]
l = ArrayList@0x7fff5fbff8c0[1](Connecting...,Connecting... the dots.,ArrayList@0x100100b00[1](),ClassCastException@0x100100b20[1],Created in other scope)
Program ended with exit code: 0
Fundamental Design
Java is (with the exception of primitive types like int, double etc. and their array forms) an object-oriented language. Everything in Java or Objective-C derives from a common and well known base class. So there is a base class Object in this example as well and there is an equivalent of the platform type String as well as a sample collection called ArrayList which is holding just Objects.
One thing is very important: There is no need for pointers as memory management is part of the solution! For instance, the ClassCastException that is thrown can be added to the ArrayList without having to worry about leaking memory afterwards. It’s not complete yet though as the notion of “weak” pointers is still missing (think: ARC), but the strong part it is fully working here.
The whole idea of the implementation is based on the well-known PIMPL idiom but it also throws the idea of the smart_ptr into the mix but it even goes further. Firstly, all behavior and data are strictly separated, not just the private members. Each conceptual class like String, ArrayList or ClassCastException has a functional class that implements behavior only and no data at all, it acts like a fully functional proxy to the data. This makes it possible to clone (copy assign) these proxy objects very cheaply, because they consist only of 2 pointers (data and _vtable). The actual data is implemented in the nested class “Impl”. There is one specialized Impl class for each conceptual class (1v1 mapping). Both the conceptual classes and the Impl classes span two parallel type hierarchies. As one picture says more like 1000 words, here it is:
In the base Impl (Object::Impl) a smart_ptr like reference counting is implemented (_refCount). I have explicitly added two methods Object::retain() and Object::release() in the code to express the similarity to Objective-C’s NSObject, but this is all handled internally during copy construction or assignment.
Conclusion
I have still to decide wether an approach like this is generally feasible. What I like is to be able to clone good concepts and class library designs from other languages like Java or Objective-C into C++ and continue coding without having to worry about the aforementioned detailed C++ design decisions that trouble me every day.
Of course, I would have to implement ARC style memory management completely before it can be used, otherwise cyclical references would leak. Also, I’d like to mention that I’m fully aware that a coding style like this leads to immediate code bloat. But so does the PIMPL idiom. In order to mitigate that, I have a flexible code generator on my side which let’s me do most part of the actual coding in UML as opposed to hand-crafting it where I would definitely think twice or even more before traveling down this road…
View the full source: https://gist.github.com/2279561