1. Using Classes

This page provides examples of creating and using classes in the CMakePP language.

1.1. Writing a Basic Class

We’ll begin by writing a simple class Automobile that only contains one attribute named color that takes the default value red:

# Begin class definition
cpp_class(Automobile)

    # Define an attribute "color" with the default value "red"
    cpp_attr(Automobile color red)

# End class definition
cpp_end_class()

Now we can instantiate a Automobile object called my_auto, access its color attribute, and print out that value:

# Create an instance of the class called "my_auto" using the default CTOR
Automobile(CTOR my_auto)

# Access the "color" attribute and save it to the var "my_autos_color"
Automobile(GET "${my_auto}" my_autos_color color)

# Print out the value of the var "my_autos_color"
message("The color of my_auto is: ${my_autos_color}")

# Output: The color of my_auto is: red

We can also set the value of the attribute:

# Set a new value for the "color" attribute
Automobile(SET "${my_auto}" color blue)

# User-defined class names are case-insensitive, so lowercase works, too
automobile(GET "${my_auto}" my_autos_color color)

# Print out the value of the var "my_autos_color"
message("The color of my_auto is: ${my_autos_color}")

# Output: The color of my_auto is: blue

Note

Class names are case-insensitive, so Automobile, automobile, and AUTOMOBILE are all valid ways to refer to the class, as shown in the last code snippet above.

See Classes for more information about CMakePP classes.

1.2. Adding a Member Function

Next we will add a function to our class. The function will be named start and will simply print a message indicating that our Automobile has started its engine. The updated class definition with this new function added is:

# Begin class definition
cpp_class(Automobile)

    # Define an attribute "color" with the value "red"
    cpp_attr(Automobile color red)

    # Define a function "start" that prints a message
    cpp_member(start Automobile)
    function("${start}" self)
        message("Vroom! I have started my engine.")
    endfunction()

# End class definition
cpp_end_class()

After creating an instance of the Automobile class named my_auto (as we did in the previous example) we can call our function using the following:

# Create an instance of the class called "my_auto" using the default CTOR
Automobile(CTOR my_auto)

# Call the function using our Automobile instance
Automobile(start "${my_auto}")

# Output: Vroom! I have started my engine.

See Member Functions for more information about writing class member functions.

1.3. Adding a Function That Takes an Argument

Now we will add a function called drive that takes two arguments, an int and a str and prints a message using those two arguments. We can do that by adding the following to our class:

# Define a function "drive" that takes an int and a str and prints a message
cpp_member(drive Automobile int str)
function("${drive}" self distance_km destination)
    message("I just drove ${distance_km} km to ${destination}!")
endfunction()

Using our Automobile instance my_auto we can call the function in the following way:

# Call the function and pass two arguments
Automobile(drive "${my_auto}" 10 "London")

# Output: I just drove 10 km to London!

See Types for a list and descriptions of data types supported by the CMakePP language and Member Functions for more information about writing class member functions.

Note

The CMakePP language will throw an error if it cannot find a function whose signature matches the call you are trying to make. In other words, the name of the function you are calling and the types of arguments you are passing in must match the function name and argument types in the function definition.

1.4. Adding a Function That References Attributes

Functions can access attributes of the class they are a member of. We will add an attribute km_driven to our class. We can then add a function describe_self that prints a message describing the color of the car and how far it has driven. Within our function, we’ll use the GET function, but this time we’ll pass a prefix and a list of attribute names. This call will get all the attributes and store them in the current, local scope with the prefix prepended to their name. Here is the function:

# Define a function "describe_self" that references attributes of the class
cpp_member(describe_self Automobile)
function("${describe_self}" self)

    # Access the attributes of the class and store them into the local vars
    # _ds_color and _ds_km_driven
    Automobile(GET "${self}" _ds color km_driven)

    # Print out a message
    message("I am an automobile, I am ${_ds_color}, and I have driven ${_ds_km_driven} km.")

endfunction()

This function can be accessed in the same way as previous examples:

# Call the function using the instance "my_auto"
Automobile(describe_self "${my_auto}")

# Output: I am an automobile, I am red, and I have driven 0 km.

1.5. Returning a Value from a Function

We will often want to return values from functions so that we can store those values for later use. We can modify the describe_self function we just wrote to return a value instead of printing a message.

Returning values from a function works differently in CMake than in most other languages. The best practice is to pass into the function the name of the variable that you want the return value to be stored in from the parent scope (we will refer to this name as the return identifier). Then, have the function set the value of the variable with the name specified by the return identifier in the parent scope using the set command with the PARENT_SCOPE option. This is demonstrated by the following redefinition of describe_self:

# Redefine "describe_self" to take in a return identifier
cpp_member(describe_self Automobile str)
function("${describe_self}" self return_id)

    # Access the attributes of the class and store them into local vars
    Automobile(GET "${self}" my_color color)
    Automobile(GET "${self}" my_km_driven km_driven)

    # Set the value of the var with the name ${return_id} in the parent scope
    set("${return_id}" "I am an automobile, I am ${my_color}, and I have driven ${my_km_driven} km." PARENT_SCOPE)

endfunction()

Note

When we use the dereferencing expression in code comments (such as the comments containing “${return_id}” above) or documentation, we are referring to the value contained within the variable with the name return_id. In other words, we mean to imply dereferencing the variable and getting its value in the same way that the CMake interpreter would do so.

We can call this function and access its return value using the following:

# Call the function and store its result in "my_result"
Automobile(describe_self "${my_auto}" my_result)

# Print out the value of "my_result"
message("${my_result}")

# Output: I am an automobile, I am red, and I have driven 0 km.

1.6. Adding Multiple Return Points to a Function

To include multiple return points in a function, CMake provides a return function (return()) that forces the processing of a function to stop when it is reached.

# Redefine "describe_self" to have multiple return points
cpp_member(describe_self Automobile str bool)
function("${describe_self}" self return_id include_color)

    # Access the km_driven attribute
    Automobile(GET "${self}" my_km_driven km_driven)

    if(include_color)
        # Access the color attribute
        Automobile(GET "${self}" my_color color)

        # Set the value of the var with the name ${return_id} in the current scope
        set("${return_id}" "I am an automobile, I am ${my_color}, and I have driven ${my_km_driven} km." PARENT_SCOPE)
        return()
    endif()

    # This only executes if include_color is false
    # Set the value of the var with the name ${return_id} in the current scope
    set("${return_id}" "I am an automobile and I have driven ${my_km_driven} km." PARENT_SCOPE)
    return()

endfunction()

Alternatively, we can employ the cpp_return macro to create multiple return points in a function. Additionally cpp_return also provides us with a more concise way to return a value to the parent scope.

When we want to return from a function and return a value to the variable with the name ${return_id} to the parent scope we just need to do the following:

  1. Set the value of the variable with the name ${return_id} in the current scope to the value we want to return

  2. Call cpp_return(${return_id})

This will set the value of the variable with the name ${return_id} in the parent scope to that value it had in the function’s scope as well as return control to the parent scope.

Suppose we wanted our describe_self function to take in an option that specifies whether or not it should indicate the color of itself in the description it returns. We could accomplish this by redefining the function as follows:

# Redefine "describe_self" to have multiple return points
cpp_member(describe_self Automobile str bool)
function("${describe_self}" self return_id include_color)

    # Access the km_driven attribute
    Automobile(GET "${self}" my_km_driven km_driven)

    if(include_color)
        # Access the color attribute
        Automobile(GET "${self}" my_color color)

        # Set the value of the var with the name ${return_id} in the current scope
        set("${return_id}" "I am an automobile, I am ${my_color}, and I have driven ${my_km_driven} km.")

        # Return the value and exit the function
        cpp_return("${return_id}")
    endif()

    # This only executes if include_color is false
    # Set the value of the var with the name ${return_id} in the current scope
    set("${return_id}" "I am an automobile and I have driven ${my_km_driven} km.")

    # Return the value and exit the function
    cpp_return("${return_id}")

endfunction()

We can call the function in the following way:

# Call the function and specify that color should be included
Automobile(describe_self "${my_auto}" my_result TRUE)
message("${my_result}")

# Output: I am an automobile, I am red, and I have driven 0 km.

# Call the function and specify that color should NOT be included
Automobile(describe_self "${my_auto}" my_result FALSE)
message("${my_result}")

# Output: I am an automobile and I have driven 0 km.

1.7. Overloading a Function

We can overload a function by adding a function of the same name with a different signature. For example, we can overload our function start by adding a new function definition with the same name that takes one argument instead of no arguments. This can be done by adding the following to our class definition:

# Overload the "start" function
cpp_member(start Automobile int)
function("${start}" self distance_km)
    message("Vroom! I started my engine and I just drove ${distance_km} km.")
endfunction()

Now we can call the new function by passing in arguments with the correct types to match the signature of the new function we wrote. In this case we need to pass in one integer to match the new signature:

# Call the new function implementation
Automobile(start "${my_auto}" 10)

# Output: Vroom! I started my engine and I just drove 10 km.

# We can still call the original function implementation as well
Automobile(start "${my_auto}")

# Output: Vroom! I have started my engine.

1.8. Adding a User-Defined Constructor

The CMakePP language allows users to define multiple custom constructors for classes. This is done using the cpp_constructor command. Here we add a constructor that takes a new color to our Automobile class:

cpp_constructor(CTOR Automobile desc)
function("${CTOR}" self new_color)
    # Set the color attribute to the value provided
    Automobile(SET "${self}" color "${new_color}")
endfunction()

Multiple constructors can be added to a class. Calls to constructors will use function resolution in the same way the member function calls do. That is when a call is made to a constructor, the CMakePP language will attempt to find a constructor that matches the signature of that call and then call that constructor. If no matching constructor is found, an error will be thrown. The only exception to this is when a call is made to the constructor of a class and no arguments are passed. In that case, the CMakePP language will just call the default constructor for the class:

# Create an instance of the class called "my_auto" using the default CTOR
Automobile(CTOR my_auto)

Automobile(GET "${my_auto}" my_color color)

# 'color' is the default 'red'

# Create an instance of the class called "my_auto_2" using the default
# user-defined CTOR
Automobile(CTOR my_auto_2 blue)

Automobile(GET "${my_auto_2}" my_color color)

# 'color' is the provided 'blue'

1.9. Using the KWARGS Constructor

The CMakePP language allows users to call a KWARGS Constructor. This constructor enables users to automatically set the values of attributes of the class upon construction. No constructor needs to be defined to use this feature. We just need to use the KWARGS keyword as the third argument to the call and provide a list consisting of the name of each attribute we want to set followed immediately by the value or values we want to set. Suppose our automobile class has three attributes: color, num_doors, and owners, along with a describe_self method to print these values:

# Begin class definition
cpp_class(Automobile)

    # Define an attribute "color" with the value "red"
    cpp_attr(Automobile color red)

    # Define an attribute "num_doors" with a starting value of 4
    cpp_attr(Automobile num_doors 4)

    # Define an attribute "owners" with a blank starting value
    cpp_attr(Automobile owners "")

    # Redefine "describe_self" to have multiple return points
    cpp_member(describe_self Automobile str)
    function("${describe_self}" self return_id)

        # Access the color attribute
        Automobile(GET "${self}" my_color color)

        # Access the num_doors attribute
        Automobile(GET "${self}" my_num_doors num_doors)

        # Access the owners attribute
        Automobile(GET "${self}" my_owners owners)

        # Set the value of the var with the name ${return_id} in the current scope
        set("${return_id}" "I am a ${my_color} automobile with ${my_num_doors} doors, owned by ${my_owners}." PARENT_SCOPE)

    endfunction()

# End class definition
cpp_end_class()

Then we could set these upon construction using the following:

Automobile(CTOR my_auto KWARGS color red num_doors 4 owners Alice Bob Chuck)

# We can still call the original function implementation as well
Automobile(describe_self "${my_auto}" my_result)

# Print out the value of "my_result"
message("${my_result}")

# Output: I am a red automobile with 4 doors, owned by Alice;Bob;Chuck.

This would set the value of color to red, num_doors to 4, and owners to Alice;Bob;Chuck.

1.10. Writing a Derived Class

The CMakePP language supports inheritance which enables us to write subclasses that are derived from (or inherit from) a base class. Subclasses inherit all attributes and functions from their base class. However, subclasses can override the definitions of functions in their base classes. They can also override the default values of attributes that are set in the base class.

We can demonstrate this by creating a new Car class that is derived from our Automobile class. Our Car class will contain a new attribute num_doors and will override the describe_self method to provide a more precise description. We can define the class by writing the following:

# Begin class definition
cpp_class(Car Automobile)

    # Override the default value of the color attribute
    cpp_attr(Automobile color green)

    # Add a new attribute to the subclass
    cpp_attr(Car num_doors 4)

    # Override the "describe_self" method of the Automobile class
    cpp_member(describe_self Car str)
    function("${describe_self}" self result_id)
        # Get inherited and new attributes
        Car(GET "${self}" my_color color)
        Car(GET "${self}" my_km_driven km_driven)
        Car(GET "${self}" my_num_doors num_doors)

        # Set the return value
        set("${result_id}" "I am a car with ${my_num_doors} doors, I am ${my_color}, and I have driven ${my_km_driven} km." PARENT_SCOPE)
    endfunction()

# End class definition
cpp_end_class()

We can now create an instance of our derived Car class and access its methods (and the methods inherited from its base class) through the Car class:

# Create an instance of the derived class "Car"
Car(CTOR my_car)

# Access the overridden method "describe_self" through the derived class
Car(describe_self "${my_car}" car_result)
message("${car_result}")

# Output: I am a car with 4 doors, I am green, and I have driven 0 km.

# Access the inherited method "start" through the derived class
Car(start "${my_car}")

# Output: Vroom! I have started my engine.

Alternatively we can access the methods of the Car class through its base class Automobile:

# Access the overridden method "describe_self" through the base class
Automobile(describe_self "${my_car}" auto_result)
message("${auto_result}")

# Output: I am a car with 4 doors, I am red, and I have driven 0 km.
# Access the inherited method "start" through the base class
Automobile(start "${my_car}")

# Output: Vroom! I have started my engine.

1.11. Inheriting from Multiple Classes

1.11.1. The Basics

A class can inherit from multiple parent classes. Suppose we have defined the following classes to represent electric vehicles and trucks:

# The ElectricVehicle class
cpp_class(ElectricVehicle)

    # Attribute for storing battery percentage
    cpp_attr(ElectricVehicle battery_percentage 100)

    # Function for starting the vehicle
    cpp_member(drive ElectricVehicle)
    function("${drive}" self)
        message("I am driving.")
    endfunction()

cpp_end_class()

and

# The Truck class
cpp_class(Truck)

    # Attribute for storing towing capacity
    cpp_attr(Truck towing_cap_lbs 3500)

    # Function for driving the truck
    cpp_member(tow Truck)
    function("${tow}" self)
        message("I am towing.")
    endfunction()

cpp_end_class()

If we want to create a class to represent an electric truck, we can create a new class ElectricTruck that inherits from both of these classes:

# Define a subclass that inherits from both parent classes
cpp_class(ElectricTruck ElectricVehicle Truck)

    # This is an empty class that inherits methods and attributes from its parent classes

cpp_end_class()

Then we can create an instance of ElectricTruck like we would any other class:

# Create instance of the subclass
ElectricTruck(CTOR my_inst)

We can then access the attributes that are defined in each of the parent classes like we would any other attribute:

# Access the attributes of each parent class through the ElectricTruck class
ElectricTruck(GET "${my_inst}" result1 battery_percentage)
message("Battery percentage: ${result1}%")
ElectricTruck(GET "${my_inst}" result2 towing_cap_lbs)
message("Towing capactiy: ${result2} lbs")

# Output:
# Battery percentage: 100%
# Towing capactiy: 3500 lbs

We can access the functions defined in each of the parent classes as well:

# Access the functions of each parent class through the ElectricTruck class
ElectricTruck(drive "${my_inst}")
ElectricTruck(tow "${my_inst}")

# Output:
# I am driving.
# I am towing.

1.11.2. Inheriting from Multiple Classes with Conflicting Attribute and Function Names

Inheriting from multiple classes creates the possibility of inheriting from two or more classes that all have an attribute of the same name or a function with the same signature. Suppose our ElectricVehicle and Truck classes were defined with the following:

# Define the ElectricVehicle class
cpp_class(ElectricVehicle)

    # Attribute for storing the power source of the electric vehicle
    cpp_attr(ElectricVehicle power_source "100 kWh Battery")

    # Function for starting the vehicle
    cpp_member(start ElectricVehicle)
    function("${start}" self)
        message("I have started silently.")
    endfunction()

cpp_end_class()

and

# Define the Truck class
cpp_class(Truck)

    # Attribute for storing the power source of the truck
    cpp_attr(Truck power_source "20 Gallon Fuel Tank")

    # Function for starting the truck
    cpp_member(start Truck)
    function("${start}" self)
        message("Vroom! I have started my engine.")
    endfunction()

cpp_end_class()

Notice that both classes have an attribute named power_source and a function named start. Again, we can create a subclass of these two classes using the following:

# Define a subclass that inherits from both parent classes
cpp_class(ElectricTruck ElectricVehicle Truck)

    # This is an empty class that inherits methods and attributes from its parent classes

cpp_end_class()

Now if we attempt to access the power_source attribute or call the start function, the CMakePP language will search the parent classes in the order that they were passed to the cpp_class macro. That is, the CMakePP language will first look in the ElectricVehicle class for the attribute or function and, if it does not find the attribute for function there, the CMakePP language will then move on to the Truck class.

So, if we create an instance of ElectricTruck and attempt to access power_source and call start we’ll get the following:

# Create instance of the subclass
ElectricTruck(CTOR my_inst)

# Access the power_source attribute
ElectricTruck(GET "${my_inst}" result power_source)
message("Power source: ${result}")

# Output
# Power source: 100 kWh Battery

ElectricTruck(start "${my_inst}")

# Output
# I have started silently.

Alternatively, we could define our subclass with cpp_class(TruckElectric Truck ElectricVehicle). Note that we now placed Truck in front of ElectricVehicle, so the CMakePP language will would look in Truck first when searching for attributes and functions:

# Create instance of the subclass
TruckElectric(CTOR my_inst)

# Access the power_source attribute
TruckElectric(GET "${my_inst}" result power_source)
message("Power source: ${result}")

# Output
# Power source: 20 Gallon Fuel Tank

TruckElectric(start "${my_inst}")

# Output
# Vroom! I have started my engine.

1.12. Adding A Pure Virtual Member Function

The CMakePP Language allows users to define pure virtual member functions in base classes. These functions have no concrete implementation, but can be overriden by implementations in derived classes. Let’s start by defining a Vehicle class with a virtual member function describe_self:

# Define the Vehicle class
cpp_class(Vehicle)

    # Add a virtual member function to be overridden by derived classes
    cpp_member(describe_self Vehicle)
    cpp_virtual_member(describe_self)

cpp_end_class()

Now we can define a Truck class that is derived from the Vehicle class that overrides describe_self with an implementation:

# Define the Truck class
cpp_class(Truck Vehicle)

    cpp_member(describe_self Truck)
    function("${describe_self}" self)
        message("I am a truck!")
    endfunction()

cpp_end_class()

Now we can create an instance of the Truck class and call the describe_self function:

# Create an instance of the Truck class and call describe_self
Truck(CTOR my_inst)
Truck(describe_self "${my_inst}")

# Output: I am a truck!

# Call from parent
Vehicle(describe_self "${my_inst}")

# Output: I am a truck!

Warning

If a call is made to the describe_self function for an instance of the Vehicle class, the CMakePP language will throw an error indicating that this function is virtual and must be overridden in a derived class.