Written by
Niels Moseley
on
on
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