Generating C++ data classes and visitor

Writing hierarchical data classes and a visitor in C++ involves a lot of boilerplate code. With C++17 we have std::variant, std::visit and lambdas, but that just looks ugly to me and it certainly isn’t beginner friendly. In my opinion the traditional derive-from-base-class and virtual functions paradigm for accept and visit is more readable and easier to understand.

The effort of writing everything out can be handled by a code generator. Here is one in Python:

#!/usr/bin/python3
#
# Code generator for data classes and their visitor
# Moseley Instruments (c) 2024
#

import io
import sys
import tomli


def genPredecl(out : io.BufferedWriter, clsData):
    className = clsData[0]
    out.write(f"class {className};\n")

# Add visit() method to class
def genVisitorMethod2(out : io.BufferedWriter, clsData):
    className = clsData[0]
    out.write(f"    virtual void visit({className} *obj) {{}};\n")

# Add visit() method to class
def genConstVisitorMethod2(out : io.BufferedWriter, clsData):
    className = clsData[0]
    out.write(f"    virtual void visit(const {className} *obj) {{}};\n")


## ##############################################################################################################
## Base class creation functions
## ##############################################################################################################

def genBaseClass(out : io.BufferedWriter, baseclassName : str, visitorName : str):
    out.write(
        f"struct {baseclassName}\n"
        "{\n"
        f"    virtual void accept({visitorName} &visitor) {{ visitor.visit(this); }};\n"
        f"    virtual void accept(Const{visitorName} &visitor) const {{ visitor.visit(this); }};\n"
    )

    for baseMember in baseclassMembers:
        out.write(f"    {baseMember}\n")

    out.write(
        "};\n"
        "\n\n")

## ##############################################################################################################
## Derived class creation functions
## ##############################################################################################################

def genDerivedClass(out : io.BufferedWriter, baseclassName : str, visitorName : str, clsData):
    className = clsData[0]
    out.write(f"class {className} : public {baseclassName}\n")
    out.write("{\n")
    out.write("public:\n\n")

    # visit access
    out.write(f"    void accept({visitorName} &visitor) override {{ visitor.visit(this); }}\n")
    out.write(f"    void accept(Const{visitorName} &visitor) const override {{ visitor.visit(this); }};\n\n")

    for member in clsData[1:]:
        out.write(f"    {member}\n")

    out.write("};\n\n")

## ##############################################################################################################
## Visitor class creation
## ##############################################################################################################

def genVisitor(out : io.BufferedWriter, baseclassName : str, visitorName : str, clsData):
    out.write(
        f"struct {visitorName}\n"
        "{\n")

    genVisitorMethod2(outfile, [baseclassName])

    for cls in classes:
        genVisitorMethod2(outfile, cls)

    out.write("};\n\n")

def genConstVisitor(out : io.BufferedWriter, baseclassName : str, visitorName : str, clsData):
    out.write(
        f"struct Const{visitorName}\n"
        "{\n")

    genConstVisitorMethod2(outfile, [baseclassName])

    for cls in classes:
        genConstVisitorMethod2(outfile, cls)

    out.write("};\n\n")

## ##############################################################################################################
## MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN MAIN
## ##############################################################################################################

if (len(sys.argv) != 2):
    print("usage: gencode <file.toml>")
    exit(1)

with open(sys.argv[1], mode="rb") as fp:
    config = tomli.load(fp)

baseclassName = config["baseclass"]["name"]

if ("namespace" in config):
    namespace = config["namespace"]
else:
    namespace = ""

if ("members" in config["baseclass"]):
    baseclassMembers = config["baseclass"]["members"]
else:
    baseclassMembers = []

classes = config["classes"]
includes = config["includes"]
visitorName   = baseclassName + "Visitor"

outfilename = baseclassName.lower() + ".hpp"

with io.open(outfilename, 'w') as outfile:
    outfile.write(
        "/* Generated by 'gencode.py'\n"
        "   Copyright Niels Moseley (c) 2024\n"
        "   All rights reserved\n"
        "*/\n\n"
        "#pragma once\n"
    )

    # Write includes in header
    for inc in includes:
        outfile.write(inc + "\n")

    # Create the namespace, if there is one
    outfile.write("\n")
    if (len(namespace) != 0):
        outfile.write(f"namespace {namespace}\n")
        outfile.write("{\n\n")

    # Generate predeclarations
    genPredecl(outfile, [baseclassName])
    for cls in classes:
        genPredecl(outfile, cls)

    # Define visitor
    outfile.write("\n\n")
    outfile.write("// Visitor interface\n")
    genVisitor(outfile, baseclassName, visitorName, cls)
    outfile.write("\n\n")
    genConstVisitor(outfile, baseclassName, visitorName, cls)
    outfile.write("\n\n")

    # Define node base class
    outfile.write("// Base class\n")
    genBaseClass(outfile, baseclassName, visitorName)

    # Define derived classes
    outfile.write("// Derived classes\n")
    for cls in classes:
        genDerivedClass(outfile, baseclassName, visitorName, cls)

    # Close namespace
    if (len(namespace) != 0):
        outfile.write("}; //end namespace\n\n")

outfile.close()

An example TOML specification file for all the classes etc. It should be self-explanatory.

# Generator for data classes and a visitor

# optional namespace
namespace = "MyNamespace"

# optional includes
includes = [
    "#include <memory>"
]

# define your classes and members here
classes = [
    ["UnaryOp", "std::unique_ptr<ASTNode> m_exp;"],
    ["BinaryOp", "std::unique_ptr<ASTNode> m_left;", "std::unique_ptr<ASTNode> m_right;"]
]

# define the base class here
[baseclass]
name = "ASTNode"
members = ["int m_test{0};"]

results in:

/* Generated by 'gencode.py'
   Copyright Niels Moseley (c) 2024
   All rights reserved
*/

#pragma once
#include <memory>

namespace MyNamespace
{

class ASTNode;
class UnaryOp;
class BinaryOp;


// Visitor interface
struct ASTNodeVisitor
{
    virtual void visit(ASTNode *obj) {};
    virtual void visit(UnaryOp *obj) {};
    virtual void visit(BinaryOp *obj) {};
};



struct ConstASTNodeVisitor
{
    virtual void visit(const ASTNode *obj) {};
    virtual void visit(const UnaryOp *obj) {};
    virtual void visit(const BinaryOp *obj) {};
};



// Base class
struct ASTNode
{
    virtual void accept(ASTNodeVisitor &visitor) { visitor.visit(this); };
    virtual void accept(ConstASTNodeVisitor &visitor) const { visitor.visit(this); };
    int m_test{0};
};


// Derived classes
class UnaryOp : public ASTNode
{
public:

    void accept(ASTNodeVisitor &visitor) override { visitor.visit(this); }
    void accept(ConstASTNodeVisitor &visitor) const override { visitor.visit(this); };

    std::unique_ptr<ASTNode> m_exp;
};

class BinaryOp : public ASTNode
{
public:

    void accept(ASTNodeVisitor &visitor) override { visitor.visit(this); }
    void accept(ConstASTNodeVisitor &visitor) const override { visitor.visit(this); };

    std::unique_ptr<ASTNode> m_left;
    std::unique_ptr<ASTNode> m_right;
};

}; //end namespace