📄 Software · Code Architecture

Organizing Code Across Files

Every team that grows past one mechanism hits the same wall: main.cpp becomes 500 lines of chaos. Splitting code into subsystem files makes it readable, maintainable, and debuggable — skills that transfer directly to real-world software engineering.

1
Why Split
2
Headers
3
Subsystems
4
Extern
5
Full Structure
// Section 01
Why One Big File Stops Working
Every PROS project starts with a single main.cpp. For a clawbot with one mechanism, that is fine. For a robot with a drive, intake, arm, and claw — all with their own PID loops, macros, and state variables — one file becomes unmanageable fast.
💡
The real cost of one big file: when something breaks at competition and you have 8 minutes before your next match, you should be able to find the intake code in under 10 seconds. If everything is in one 800-line file, that search takes longer — and costs matches.

Signs Your Code Needs to Be Split

The Goal: One File Per Subsystem

A well-organized VRC project looks like this:

src/
  main.cpp — opcontrol(), autonomous(), initialize() only
  intake.cpp — all intake functions and state
  arm.cpp — all arm/lift functions and state
  claw.cpp — claw/endgame functions
  autons.cpp — autonomous routines (already separate in EZ Template)
include/
  main.h — PROS + EZ Template includes
  intake.hpp — intake function declarations
  arm.hpp — arm function declarations
  claw.hpp — claw function declarations

Each subsystem has exactly two files: a .cpp file with the actual code, and a .hpp header file that declares what functions exist. Any other file that needs the intake just includes the header — it does not need to know how the intake works internally.

// Section 02
Header Files — The Declaration Layer
A header file (.hpp) is a promise: "these functions exist, here is what they take and return." It does not contain the actual code — just the declarations. This is what lets other files use your subsystem without needing to include all its implementation.

Anatomy of a Header File

include/intake.hpp
#pragma once // Header guard — prevents double-inclusion (explained next) // Declare the functions that intake.cpp will define // Other files include this header to call these functions void intakeForward(); // run intake forward at full speed void intakeReverse(); // run intake backward void intakeStop(); // stop intake void intakeSpeed(int speed); // run at specific speed -127 to 127 bool intakeHasElement(); // returns true if optical sensor detects game element

What #pragma once Does

#pragma once is a header guard. It tells the compiler: "only include this file once, even if multiple other files include it." Without it, if both main.cpp and arm.cpp include intake.hpp, the compiler sees the declarations twice and throws a duplicate definition error.

Always put #pragma once as the very first line of every .hpp file you create. It is simpler than the older #ifndef/#define/#endif pattern and works in all modern compilers including PROS.

Rule: if you write a function in a .cpp file that any other file needs to call, declare it in the matching .hpp header. If it is only used internally within that .cpp file, do not put it in the header — keep it private to that file by declaring it static at the top of the .cpp.

Using a Header in Another File

src/main.cpp
#include "main.h" #include "intake.hpp" // now main.cpp can call intake functions #include "arm.hpp" #include "claw.hpp" void opcontrol() { while (true) { chassis.opcontrol_tank(); if (master.get_digital(DIGITAL_R1)) intakeForward(); else if (master.get_digital(DIGITAL_R2)) intakeReverse(); else intakeStop(); armUpdate(); // arm.cpp handles its own state internally clawUpdate(); pros::delay(10); } }

main.cpp is now clean and readable. It describes what happens during a match — not how each mechanism works. The how lives in each subsystem file.

// Section 03
Writing a Subsystem .cpp File
The .cpp file is where the actual code lives. It includes its own header, any needed PROS headers, and defines everything declared in the .hpp.

A Complete Intake Subsystem

src/intake.cpp
#include "main.h" // PROS types and motor declarations #include "intake.hpp" // this file's own header // ── Private state (only visible inside this file) ───────────────────── static bool intakeRunning = false; // ── Function definitions ─────────────────────────────────────────────── void intakeForward() { intake.move(127); intakeRunning = true; } void intakeReverse() { intake.move(-127); intakeRunning = false; } void intakeStop() { intake.move(0); intakeRunning = false; } void intakeSpeed(int speed) { intake.move(speed); intakeRunning = (speed != 0); } bool intakeHasElement() { // optical sensor hue check — tune threshold to your game element color return intakeSensor.get_proximity() > 200; }
💡
The static keyword on intakeRunning means it is private to intake.cpp. No other file can access or accidentally modify it. This is encapsulation — the same concept taught in every computer science course. Use static on any variable or helper function that should not be visible outside the file.

An Arm Subsystem With State Machine Logic

src/arm.cpp
#include "main.h" #include "arm.hpp" // Private state static int armTarget = 0; static bool holdPosition = false; void armGoTo(int position) { armTarget = position; holdPosition = true; arm.move_absolute(position, 100); } void armManual(int speed) { holdPosition = false; arm.move(speed); } // Called from opcontrol() every loop — handles hold logic void armUpdate() { if (holdPosition) { // motor.move_absolute() handles this internally, // but we can override if manual input is given int stick = master.get_analog(ANALOG_LEFT_Y); if (abs(stick) > 20) { armManual(stick); // driver overrides preset hold } } }
// Section 04
Sharing Motors Across Files With extern
The V5 motor objects — intake, arm, chassis motors — are declared once somewhere. If your subsystem files need to use them, they need to know they exist. That is what extern is for.

The Problem: Motor Declared in One File, Needed in Another

In EZ Template projects, motors and sensors are typically declared in robot-config.cpp. When intake.cpp tries to call intake.move(127), the compiler asks: what is intake? Where was it declared?

There are two clean solutions:

Solution A — Declare Motors in a Shared Header (Recommended for EZ Template)

In EZ Template projects, motor objects are typically declared in robot-config.cpp and already accessible via main.h. Check your project's main.h — if the motors are declared there or in a header it includes, your subsystem .cpp files can access them by just including main.h.

include/main.h — motors declared here (typical EZ Template setup)
// Already in your project — EZ Template puts these here extern pros::Motor intake; extern pros::Motor arm; extern pros::Motor claw;

Solution B — extern in the Subsystem Header (For Motors Not in main.h)

If you add a new motor that is not in main.h, declare the motor object in robot-config.cpp and add an extern declaration to your subsystem header:

src/robot-config.cpp — the actual motor object lives here
// Defined once — this is the real object pros::Motor intake(3, pros::E_MOTOR_GEARSET_18, false);
include/intake.hpp — tells other files the motor exists
#pragma once #include "pros/motors.hpp" // extern = "this variable exists, it was defined somewhere else" // NOT a new declaration — just a reference to the existing one extern pros::Motor intake; void intakeForward(); void intakeReverse(); void intakeStop();
⚠️
Never define (create) the same motor object in two .cpp files. extern is a declaration — it says the object exists elsewhere. The actual object should only be created (defined) in one place, typically robot-config.cpp. Creating it twice causes a "multiple definition" linker error.

Quick Mental Model

// Section 05
The Full Project Structure
Putting it all together — a complete 3-mechanism robot with clean file organization. Every file has a single clear responsibility.

Complete File List for a Drive + Intake + Arm + Claw Robot

include/
  main.h — PROS system includes, chassis declaration, master controller
  intake.hpp — #pragma once, extern motor if needed, function declarations
  arm.hpp — #pragma once, extern motor if needed, function declarations
  claw.hpp — #pragma once, extern motor if needed, function declarations
src/
  main.cpp — initialize(), autonomous(), opcontrol() — thin, readable
  robot-config.cpp — all motor and sensor object definitions
  autons.cpp — autonomous routines (EZ Template default)
  intake.cpp — intake function definitions, private state
  arm.cpp — arm function definitions, position presets, hold logic
  claw.cpp — claw toggle, endgame sequence

The Resulting main.cpp — Clean and Readable

src/main.cpp — the whole file, for a 4-mechanism robot
#include "main.h" #include "intake.hpp" #include "arm.hpp" #include "claw.hpp" void initialize() { ez::ez_template_print(); chassis.opcontrol_curve_buttons_toggle(true); chassis.initialize(); } void autonomous() { chassis.pid_targets_reset(); chassis.drive_imu_reset(); selector.call_auton(); } void opcontrol() { while (true) { chassis.opcontrol_tank(); // Intake if (master.get_digital(DIGITAL_R1)) intakeForward(); else if (master.get_digital(DIGITAL_R2)) intakeReverse(); else intakeStop(); // Arm and claw update their own state each loop armUpdate(); clawUpdate(); pros::delay(10); } }
🏆
Competition debugging speed. When intake stops working 10 minutes before a match, you open intake.cpp — 60 lines, one mechanism. You fix it in 4 minutes. When everything is in one 700-line file, you spend 6 of those minutes just finding the right section.

When to Split a File

⚙ STEM Highlight Software Engineering: Modularity, Coupling & Cohesion
Code organization applies two software engineering principles: cohesion (code that does related things lives together) and coupling (modules depend on each other as little as possible). A single 800-line main.cpp has low cohesion (everything mixed together) and implicitly high coupling (changes anywhere can affect anything). Splitting into arm.cpp, intake.cpp, chassis.cpp creates high cohesion, low coupling — the gold standard. #pragma once implements an include guard: a compile-time mechanism to prevent duplicate declarations when a header is included by multiple files.
🎤 Interview line: “We structured our code around high cohesion and low coupling — each subsystem file contains only its own logic, and files communicate through declared interfaces in header files. This mirrors professional software architecture: classes have single responsibilities and dependencies are explicit.”
🔬 Check for Understanding
You define pros::Motor arm(5); in arm.cpp. Your auton.cpp needs to use the arm motor. What is the correct approach?
Copy the motor definition into auton.cpp
Declare extern pros::Motor arm; in arm.h, include arm.h in auton.cpp — this tells the linker the variable exists elsewhere without creating a duplicate
Use a global variable file that defines all motors
Pass the motor as a function parameter every time
Related Guides
🆕 Code Style & Autoformatting → 🏷 Naming Conventions → 💾 Git & Version Control →
← ALL GUIDES