Erik Explores

Erik Explores

Share this post

Erik Explores
Erik Explores
Object-Oriented Carbon, C++ and Go Compared

Object-Oriented Carbon, C++ and Go Compared

Creating class or type hierarchies in the Go, C++ and Carbon programming languages

Erik Engheim's avatar
Erik Engheim
Aug 12, 2022
∙ Paid

Share this post

Erik Explores
Erik Explores
Object-Oriented Carbon, C++ and Go Compared
Share

Let us go through a simple example of creating an abstract base class representing a rocket engine with some concrete implementations in Go, C++ and the new Carbon language from Google. To be accurate, Carbon is not an official Google project, but done by people working at Google.

There is no Carbon compiler presently and the interpreter is very bare bones with a lot of missing functionality, so the code I am writing here is based on the language specification, rather than being verified by a compiler. Keep that in mind as there are possible errors.

In my upcoming programming book, Julia as a Second Language, I take the reader through building a space rocket in code. The code project came about specifically to teach readers about how to model type hierarchies and understand how to do object composition.

Type hierarchies and object composition for a staged rocket
Type hierarchies and object composition for a staged rocket

I have found this example useful enough to replicated it in Go code, intending to some day writing a Go programming book with this theme. In this article, I will model as subset of this type hierarchy and object-composition in C++ and Carbon.

Since the whole type hierarchy example is explored over multiple chapters in my Julia book, we can only cover a small subset of it in this story. I have chosen to zoom in on the type hierarchy for rocket engines.

UML diagram of rocket engines
UML diagram of rocket engines

A rocket engine cluster is a collection of multiple engines. The Saturn V moon rocket for instance had an engine cluster of five F1 engines in the first stage. A modern Falcon 9 rocket from SpaceX has nine Merlin engines in the first stage and a single Merlin engine in the second stage.

Defining Interfaces

The original Go code defines an abstract interface Engine for all concrete engines Merlin and CustomeEngine to implement.

// Go code - Base interface
package engine

// Force that causes 1kg to move 1m in one second.
type Newton float64

// standard measurement unit for mass, a kilogram
type Kg float64

type Engine interface {
    Mass() Kg       // Mass of whole engine
    Thrust() Newton // How much engine pushes
    Isp() float64   // Fuel efficiency of engine
}

You may notice that I am utilizing the ability of Go to let you define units like Newton and Kg, which prevents developers from accidentally mixing incompatible unites. For instance, I cannot, accidentally, add mass to thrust without getting a compilation error.

In C++ we don’t have this ability to create type safety for units, unless we define whole new classes for each unit type. Instead, the ported C++ code will use typedef. A typedef is just like an alias and does not offer any actual type safety. C++ does not have interfaces like Go and Java. Thus, we must define an abstract class instead. We do that by adding virtual methods which are not implemented.

// C++ code - Base interface
namespace engine {

    // Force that causes 1kg to move 1m in one second.
    typedef double Newton;

    // standard measurement unit for mass, a kilogram.
    typedef double Kg;

    class Engine {
    public:
        virtual Kg Mass() = 0;   // Mass of whole engine
        virtual Newton Thrust() = 0; // Power of engine
        virtual double Isp() = 0;    // Fuel efficiency
    };
}

There are several things I don’t like about the C++ solution. Because we don’t have the concept of an interface, we need to create more boilerplate by repeating the keyword virtual for every method. We indicate that the method must be implemented with the = 0 expression as the end, which is very odd and unintuitive. namespace in C++ cause nesting, which is very impractical in my view. Deep nesting should be avoided in code, in my opinion. It is not easy to visually keep track of many levels of nesting.

The Carbon solution solves some of these problems while retaining some C++ problems and introducing new ones. Namespace nesting is avoided with the use of the package keyword. alias is like a C++ typedef and thus doesn't offer any type safety for different units.

// Carbon code - Base class
package Engine api;

alias Kg = f64;
alias Newton = f64;

abstract class Engine {
    abstract fn Mass[me: Self]() -> Kg;
    abstract fn Thrust[me: Self]() -> Kg;
    abstract fn Isp[me: Self]() -> f64;
}

There are several important differences from Go and C++ you will notice:

  • Our package has an extra keyword api attached, which is used to make all type and functions declared in the file public by default. In Go, capitalized types and functions are public.

  • You clarify that a function is a method rather than a class method by adding [me: Self].

  • Methods which are not implemented must be marked as abstract.

Relative to Go and C++ I think the use of [me: Self] is a negative. It adds verbosity and noise to the code. On the other hand, using introducer keywords such as fn means method names easily aligns and become easier to scan. In C++ programmers are forced to indent their code manually to achieve the same.

I will also say that using the abstract keyword for the whole class makes it more explicit what the class is for. In C++ classes are abstract implicitly from the use of unimplemented virtual methods.

Three Merlin rocket engines at SpaceX used on the Falcon 9 rocket.

Implementation Classes

Let us look at how we can implement our Engine interface in various concrete classes. We will first look at an engine implementation consisting of only method implementation and not data members.

Go uses what is called structural typing, which means you implement an interface simply by having all the methods defined on an interface. You don’t need explicitly state that you are implementing the interface.

// Go code - Merlin rocket engine
// Engine used on the Falcon 9 rocket made by SpaceX
type Merlin struct {
}

// The mass of the rocket engine.
func (engine Merlin) Mass() Kg {
	return 470
}

// Think of this as similar to the horse power of a car.
func (engine Merlin) Thrust() Newton {
	return 845e3
}

// Specific impulse of the rocket engine. 
// A measure of fuel efficiency.
func (engine Merlin) Isp() float64 {
	return 282
}

Both C++ and Carbon uses nominal typing, which means we must explicitly state what class or interface we are implementing. Since C++ we have had the override keyword which allows the compiler to check if the method we are implementing is, in fact, overriding a function defined in a based interface as virtual. However, there is no requirement to use override, so here we got a potential source of bugs.

// C++ code - Merlin rocket engine
class Merlin : public Engine {
public:
  Engine() {}   // constructor
  ~Engine() {}  // destructor

  Kg Mass() override {
  	return 470;
  }
  
  Newton Thrust() override {
    	return 845e3;
  }
  
  double Isp() override {
    	return 282;
  }
};

Unlike Go and Carbon, C++ has constructors. A constructor is always run when a C++ object is created. When the C++ object is deallocated, the destructor will be run. Go avoids destructors because it is not deterministic when a Go object is deallocated, since it uses a garbage collector to manage memory. The Go solution is to use the defer statement, which is often used to run cleanup code.

Carbon is designed to be compatible with C++ and easily interface with C++ code. For that reason, Carbon does not use garbage collection and actually has destructors. In the example below we are not showing destructors yet, but we have made a Make class method to allocate instances of Merlin on the heap.

// Carbon code - Merlin rocket engine
final class Merlin extends Engine {
    fn Make() -> Merlin* {
        let engine: Merlin = {};
        return heap.New(engine);
    }
    
    impl fn Mass[me: Self]() -> Kg {
        return 470;
    }
    
    impl fn Thrust[me: Self]() -> Newton {
        return 845000;
    }
    
    impl fn Isp[me: Self]() -> f32 {
        return 282;
    }
}

In Carbon, we use the final keyword to indicate that the class cannot be further subclasses. Using final allows for better optimization. If you have a pointer of typeMerlin, then no virtual method lookup has to be done when a class is final. In Go, every struct is implicitly final, as you cannot subclass structs. In fact, final is default in Carbon, so you don't actually have to write it.

In Carbon, we modify methods with the keywords virtual, abstract and impl. These allow for better compiler checks than you get in C++. If you don't write any of these keywords in front of a method, that means the method cannot be overridden in a subclass, nor is it overriding a method defined in a base class.

Thus, if you intend to allow subclasses to override your method later, you need to define it as either virtual or abstract. The former is just like virtual in C++. You state that the method can be overridden while offering a default implementation.

Remember, in C++ we add the odd = 0 to virtual methods when we don't implement them. In Carbon, we use the abstract keyword instead. I think that is a more sensible choice.

You may think impl is the same as override in C++, but it is not. You have to use impl in Carbon to override a method, or you get a compiler error. The Carbon compiler will catch more problems, in other words. In C++ you can unintentionally override a method.

Cluster of rocket engines on the Russian Soyuz rocket

Let us do a class with member variables. Most rockets today have multiple rocket engines at their first stage, the so-called booster stage. The Saturn V moon rocket had five F1 rocket engines. The Falcon 9 rocket has nine Merlin engines at its first stage. To simplify the modeling of a rocket, we can represent a cluster of engines as just another kind of engine. That is what a Cluster is for. The Cluster type implements all the same methods as other engine types.

We use a Count variable to keep track of how many engines there are in the cluster. In Go, we can utilize the concept of embedding, which allows us to embed another type and expose its interface.

// Go code - Rocket engine cluster
package engine

type Cluster struct {
    Engine
    count uint8
}

func (cluster *Cluster) Mass() Kg {
    return cluster.Engine.Mass() * Kg(cluster.count)
}

func (cluster *Cluster) Thrust() Newton {
    return cluster.Engine.Thrust() * Newton(cluster.count)
}

// Create a cluster. Go does not have constructors
func NewCluster(engine Engine, count uint8) *Cluster {
    cluster := Cluster{
        Engine: engine,
        count: math.Max(count, 1.0)
    }
    return &cluster
}

In this case, we have embedded the Engine interface which adds the Mass, Thrust and Isp methods to Cluster.

However, we shadow the Mass and Thrust methods explicitly. Such shadowing is not the same as overriding. If Isp had called Mass and Thrust they would still call the implementation provided by the Engine object embedded and not the one provided by the Cluster type.

To learn more about how the Cluster class is implemented in Carbon, subscribe or read the rest on Medium.

Keep reading with a 7-day free trial

Subscribe to Erik Explores to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Erik Engheim
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share