Object-Oriented Programming

C++Script provides an alternative way to write object-oriented programs.  You can of course use C++ classes if you prefer.

C++Script's OOP is quite unusual because objects don't have types in the traditional sense.  In order to create an object, you just call a function which builds up the object for you.  This is simple but also really flexible, giving factories, brokers, inheritance, closures and nested classes out of just a few simple concepts.

Objects as containers

You can create an object using the object() function, and add members and methods to it manually.  An object is just a container of name-value pairs.  As an additional restriction, members of objects must be strings (you could use a map() if you wanted to lift this restriction).  You would normally write a "constructor function" to construct objects of a particular type.  e.g. (object1.cpp)

#include <cppscript>
 
var create_a_tree(var species, var height) // Constructor function
{
    var tree = object(); 
    tree["species"] = species;
    tree["height"] = height;
    return tree;
}
 
var script_main(var)
{
    var oak = create_a_tree("oak", 12.2), maple = create_a_tree("maple", 15.0);
    writeln( "The height of the oak is " + oak["height"] );
    return 0;
}

You can specify a class name to the object() function, which can then be queried using var::class_name().  The class name is useful mainly for debugging.

Calling methods

A method is a function that is contained in an object.  From the caller's perspective, a method is a functor.  Recall that a functor is a var that contains a function.  Suppose you had an object foo, and you wanted to call the bar method on the object.  You call the method using

var foo;
foo["bar"]();

foo["bar"] means the "bar" member of the foo object, and the () means call the var as a function.  You can naturally pass arguments to the method, e.g.

foo["bar"](1,2,3);

etc.

Defining methods

Methods are defined by assigning functors to members of the object.  There is a little detail which is that functors are often "bound" (the equivalent of a non-static member function in C++) to an object.  In fact when functions are assigned to members of objects, the first argument of the function is implicitly bound.  e.g. (methods.cpp)

#include <cppscript>

var growth_rate(var tree) { return tree["height"]/tree["age"]; }

void grow(var tree, var height) { tree["height"] += height; }

var tree(var species, var height, var age)
{
    var tree = object();
    tree["species"] = species;
    tree["height"] = height;
    tree["age"] = age;
    tree["growth_rate"] = growth_rate; // Equivalent to bind(growth_rate, tree);
    tree["grow"] = grow; // Equivalent to bind(grow, tree)
    return tree;
}

var script_main(var args)
{
    var oak = tree("oak", 10, 2);
    writeln( oak["growth_rate"]() ); // 5
    oak["grow"](2);
    writeln( oak["growth_rate"]() ); // 6
    return 0;
}

Non-bound member functions (equivalent to static methods in C++) can be assigned via a var which creates an unbound functor.  e.g.

 tree["x"] = var(grow);

Binding and closures

The previous example showed how a function was bound to an object through the line

tree["grow"] = grow;

What's happening is that the value assigned to the "grow" member of tree is bind(growth_rate, tree).  You can actually write

tree["grow"] = bind(grow, tree);

which has the same effect.  What's a little unexpected is that the bound function can be used independently of the object it is bound to:

var g = oak["grow"];
g(2); // grow the oak by 2

The variable g (as well as oak["grow"]) is a closure, because the first argument (tree) of the grow() function is "bound" to a particular object.

The bind() function works behind the scenes to create closures every time you assign a function to an object.  You can also use the bind() function explicitly, and bind any number of arguments to a function.  bind() takes a functor as its first argument, and binds the number of arguments specified.  E.g.

var grow_oak_by_1 = bind(oak["grow"], 1); 

creates a var with arguments to grow() function bound with values oak, and 1.

Note that the following are equivalent:

bind( oak["grow"], 1 );
bind( grow, oak, 1 );
bind( bind(grow, oak), 1 );

Inheritance

Supposing we wanted to add some more types of tree.  We could imagine copying and pasting the tree constructor function into two more constructor functions: oak() and maple(), and tweaking the species.  That would be poor style.  Instead you should call the tree() function from the oak() function and the maple() function as follows:

// Constructor for an oak
var oak(var height, var age) { return tree("oak", height, age); }
 
// Constructor for a maple
var maple(var height, var age) { return tree("maple", height, age); }

The astute reader will notice that we already have all the tools needed for inheritance.  In effect, the oak() and maple() functions creates a specialisation of tree.  We could make the specialization as interesting as we like:

var maple_grow(var maple, var height) 
{ 
    maple["height"] += 2*height; // They grow really fast
}
 
var maple(var height, var age)
{
    var maple = tree("maple", height, age);
    maple["grow"] = maple_grow; // Override method
    maple["colour"] = "red"; // Add a member
    return maple;
}

The var::extend() method provides a convenient shorthand for this type of function.  extend() takes a sequence of name-value pairs which is assigns to the object.  The tree() and maple() functions could then be written:

var tree(var species, var height, var age)
{
    return object().extend 
        ("species", species)
        ("height", height)
        ("age", age)
        ("grow", grow) 
        ("growth_rate", growth_rate);
}
 
var maple(var height, var age)
{
    return tree("maple", height, age).extend
        ("grow", maple_grow)
        ("colour", "yellow");
}

Multiple inheritance can be handled using the += operator to merge objects, or you can just supply a base object to the constructor function.

Private variables

There isn't a built-in mechanism to make members private.  The simplest way to do this is to give members special names, and then trust that client code won't modify your data. For example

var counter()
{
    return object().extend
        ("private_value", 0)
        ("inc", counter_inc)
        ("reset", counter_reset)
        ("get", counter_get);
}

This has the drawback of being slightly inelegant, and clients could still accidentally modify your internals. If you are really paranoid therefore, you can create a "hidden object" which lies behind your interface, as follows:

var counter()
{
    var hidden = object().extend
        ("private_value", 0)
        ("inc", counter_inc)
        ("reset", counter_reset)
        ("get", counter_get);
 
    return object().extend
        ("inc", hidden["inc"])
        ("reset", hidden["reset"])
        ("get", hidden["get"]); 
}

There is now no way that client code can access the "private_value" member of the counter.  A different approach is to clone the object, which does not rebind the member functions.  As a result, client code can modify the "private_value" member as much as they want, but all of the methods are still bound to their original object, which is a different variable.  This has the effect of "sealing/finalizing" the object.

var counter()
{
    return +object().extend
        ("private_value", 0)
        ("inc", counter_inc)
        ("reset", counter_reset)
        ("get", counter_get);
}

Factories

The inheritance example suggests that constructor functions (e.g. tree(), maple(), oak()) are a kind of "type".  This analogy might be true in the basic case, but functions are much more like factories.  The point is that the caller does not know the exact type of what is returned, only that an object is returned matching the needs of the caller.  Factories could quite easily be methods, providing context for the construction.

Contrast this with the "new X" approach in strongly-typed languages.  This is really inflexible because you are forcing the system to create a particular object, when a factory or broker is usually preferable.

Dispatchers

A "dispatcher" is an object which lets you define the var methods, much like you can overload operators in C++.

Dispatchers are created using the dispatcher() function, which accepts an object which contains methods for the functions you wish to implement.

The following example (dispatcher.cpp) implements the "get_member" and "set_member" methods, to create an object with special behaviour when you get and set members of the object:

#include <cppscript>
 
var counter_get(var c) { return c["value"]++; }
 
var counter(var value) 
{
   return object("counter").extend
          ("value", value)
          ("get", counter_get);
}
 
var multicounter_get(var multicounter, var counter_name)
{
   return multicounter["counters"][counter_name]["get"]();
}
 
void multicounter_set(var multicounter, var counter_name, var value)
{
   multicounter["counters"][counter_name] = counter(value);
}
 
var multicounter()
{
   return dispatcher( 
          object("multicounter").extend
                  ("counters", map())
                  ("get_member", multicounter_get)
                  ("set_member", multicounter_set)
          );
}
 
var script_main(var)
{
   var counters = multicounter();
   counters["c1"] = 0;
   counters["c2"] = 10;
   writeln( counters["c1"] );  // 0
   writeln( counters["c2"] );  // 10
   writeln( counters["c1"] );  // 1
   writeln( counters["c2"] );  // 11
   return 0;
}

The following methods can be implemented on the dispatcher: