The parsing side of cparse (Cursor + Parser) turns text into a data structure. The code-generation side (Generator

  • Indentation) turns a data structure into text. The two are independent — you can use one without the other. A tool that parses a config file and prints a summary uses only the parsing side. A code scaffolder that reads command-line arguments and emits skeleton files uses only the generation side. A grammar-driven code generator like UMTSM uses both, but the two halves never touch each other inside cparse — the coupling happens in the subclass.

This page walks through a small but complete Generator subclass: an emitter that produces a C++ header containing an enum class from a list of enumerator names.

Input and output

Input (given as constructor arguments):

  • Header name: "Colour.hh"
  • Namespace path: ["graphics", "core"]
  • Enum type name: "Colour"
  • Enumerators: ["red", "green", "blue"]

Desired output:

#ifndef GRAPHICS_CORE_COLOUR_HH
#define GRAPHICS_CORE_COLOUR_HH

namespace graphics
{
  namespace core
  {
    enum class Colour
    {
      Red,
      Green,
      Blue
    };
  }  // namespace core
}  // namespace graphics

#endif  // GRAPHICS_CORE_COLOUR_HH

The interesting details, all handled by Generator helpers:

  • GRAPHICS_CORE_COLOUR_HH is composed from the namespace path and header name by generateHeaderGuard, including the underscore-collapse and leading-underscore strip.
  • Red, Green, Blue come from convertUpperCaseCamel applied to the lowercase inputs.
  • The indented braces come from the Indentation member being right-shifted around each nested block.

The subclass

#include <cparse/Generator.hh>
#include <cparse/Indentation.hh>

#include <fstream>
#include <string>
#include <vector>

class EnumHeaderGen : public fedem::parser::Generator
{
public:
    EnumHeaderGen(std::string const&                path,
                  std::vector<std::string> const&   namespacePath,
                  std::string                       enumName,
                  std::vector<std::string>          enumerators)
      : out_(path)
      , path_(namespacePath)
      , name_(std::move(enumName))
      , enumerators_(std::move(enumerators))
      , header_(fs::path(path).filename().string())
    {}

    // Required overrides
    std::ostream&                getStream()   override { return out_;    }
    fedem::parser::Indentation&  getIndentor() override { return indent_; }

    void generate() override
    {
        emitHeaderGuardOpen();
        emitBlankLine();
        emitNamespaceScopes(0);
        emitBlankLine();
        emitHeaderGuardClose();
    }

    // Namespace-stack pure virtuals
    void openNamespaceScope() noexcept override {
        auto const& name = namespaceNameContainer.back();
        getStream() << getIndentor() << "namespace " << name << '\n'
                    << getIndentor() << "{\n";
        getIndentor().right();
    }

    void closeNamespaceScope() noexcept override {
        auto const name = namespaceNameContainer.back();
        getIndentor().left();
        getStream() << getIndentor() << "}  // namespace " << name << '\n';
        namespaceNameContainer.pop_back();
    }

    std::string getNamespacePath() const noexcept override {
        std::string path;
        for (auto const& n : namespaceNameContainer) {
            if (!path.empty()) path += "::";
            path += n;
        }
        return path;
    }

private:
    void emitHeaderGuardOpen() {
        auto const guard = generateHeaderGuard(header_);
        getStream() << "#ifndef " << guard << '\n'
                    << "#define " << guard << '\n';
    }

    void emitHeaderGuardClose() {
        getStream() << "#endif  // " << generateHeaderGuard(header_) << '\n';
    }

    void emitBlankLine() { getStream() << '\n'; }

    // Recursive descent through the namespace path, then the enum.
    void emitNamespaceScopes(size_t depth) {
        if (depth == path_.size()) {
            emitEnum();
            return;
        }
        namespaceNameContainer.push_back(path_[depth]);
        openNamespaceScope();
        emitNamespaceScopes(depth + 1);
        closeNamespaceScope();
    }

    void emitEnum() {
        getStream() << getIndentor() << "enum class " << name_ << '\n'
                    << getIndentor() << "{\n";
        getIndentor().right();

        for (size_t i = 0; i < enumerators_.size(); ++i) {
            getStream() << getIndentor()
                        << convertUpperCaseCamel(enumerators_[i])
                        << (i + 1 < enumerators_.size() ? ",\n" : "\n");
        }

        getIndentor().left();
        getStream() << getIndentor() << "};\n";
    }

    std::ofstream                out_;
    fedem::parser::Indentation   indent_;   // default step size = 2
    std::vector<std::string>     path_;
    std::string                  name_;
    std::vector<std::string>     enumerators_;
    std::string                  header_;
};

Reading the subclass

Two data members meet the two required accessors. getStream() returns out_, an std::ofstream opened by the constructor. getIndentor() returns indent_, an Indentation with the default step size of 2. Neither Generator nor Indentation owns these — they are yours; the base class just holds pointers.

Three namespace pure virtuals give you total control over emission format. In our C++ target, openNamespaceScope() writes namespace X\n{ and right-shifts the indent; closeNamespaceScope() left-shifts and writes } // namespace X; getNamespacePath() joins with ::. A different target — say, generating Rust modules — would emit mod X { / } and join with :: or / as needed.

generate() composes the emission from small helpers. No templating engine, no string interpolation library. Each helper either calls getStream() << getIndentor() << … (an indented line) or manipulates the indent counter. The pattern reads the way the emitted code looks.

Case conversion happens at the emission point. The enumerator names arrive lowercase ("red", "green", "blue") and are converted with convertUpperCaseCamel immediately before writing them. The data model stays canonical; the presentation choice lives at the emission site.

The header guard is composed once from the namespace path plus the header name. generateHeaderGuard("Colour.hh") — when the namespace stack is ["graphics", "core"] and thus getNamespacePath() returns "graphics::core" — produces "GRAPHICS_CORE_COLOUR_HH". The dot in .hh becomes _; consecutive underscores collapse; leading underscores strip. All of it is documented in Generator::generateHeaderGuard Notes.

Using it

int main()
{
    EnumHeaderGen gen(
        "Colour.hh",
        { "graphics", "core" },
        "Colour",
        { "red", "green", "blue" }
    );
    gen.generate();
    // Colour.hh now contains the header shown at the top of this page.
}

Extending

The pattern above scales in a few natural directions:

Multiple emit passes. If you emit both a .hh and a .cpp for the same input, either instantiate two subclasses (each with its own stream) or open the second stream inside generate(). Separate instances is cleaner because getStream() cannot sensibly return two different streams.

Reading input from a parser. Replace the constructor's raw-argument list with parser output: run a Parser subclass over an input file, extract the parsed data into your subclass's members, then call generate(). UMTSM is the canonical example — parses a state machine description with cparse, emits C++ state-machine code with Generator/Indentation. The two halves never call each other directly; the coupling is entirely in the subclass.

Encoding strings safely. If your emitted code embeds user-supplied strings as C++ string literals — for tag values, error messages, resource names — use Generator::encodeCppString to escape them:

getStream() << '"' << encodeCppString(userValue) << '"';

The static method handles the standard C escape sequences and octal-encodes non-printable bytes. It is safer than assembling the string literal by hand.

Alternative naming conventions. Override generateFilename if you want snake_case.hh files instead of UpperCamel.hh, or generateHeaderGuard if you want a different guard scheme (a UUID prefix, a project tag).

What Generator does not provide

Repeating the class synopsis's Notes for emphasis — worth knowing before you reach for the wrong tool:

  • No file management. Your subclass opens the stream.
  • No error handling. generate() returns void; if you need a diagnostic collector, add one yourself.
  • No template engine. Generator emits byte by byte. If you want substitution templates, use a real templating library and let Generator handle just the case conversion and header-guard bits.
  • No language awareness. The three namespace pure virtuals are virtual precisely because different targets need different emission syntax.

See also