Enterprise Development

Scope: This lesson is about writing C++ that survives contact with production. The language does not change — the discipline does. You will see how invariants, RAII, a deliberate error strategy, structured logging, well-chosen patterns, a reproducible build, and automated tests turn working code into dependable software.

What makes code enterprise-grade

Enterprise code is written once but read, changed, and operated for years by many people. Three qualities dominate every design decision: reliability (a defect must not lose data or money), observability (when something fails, you must be able to see why), and maintainability (the next engineer must change it safely). Everything below serves one of those three goals.

Reliability and correctness

Correctness comes from invariants enforced by the compiler, not from hoping. Prefer a compile-time guarantee to a runtime check, and a runtime check to nothing. const, strong types, and static_assert cost nothing at run time yet eliminate whole classes of bug.

#include <cstdint>
#include <type_traits>

// A strong type stops you from mixing up two "int" arguments by accident.
struct UserId { std::int64_t value; };

void transfer(UserId from, UserId to, double amount);   // cannot pass the amount as an id

// Encode an assumption the compiler can check for you.
template <typename T>
void fixedWidthBuffer() {
    static_assert(sizeof(T) >= 8, "this buffer requires a 64-bit element");
}

Resource management (RAII)

The cornerstone of safe C++ is RAII: acquire a resource in a constructor, release it in the destructor, and never expose a raw resource. Files, sockets, locks, and buffers all become objects that clean themselves up — even when an exception unwinds the stack.

#include <cstdio>
#include <stdexcept>

class FileGuard {                       // owns a FILE*; cannot be leaked or forgotten
public:
    explicit FileGuard(const char *path, const char *mode)
        : file_(std::fopen(path, mode)) {
        if (!file_) throw std::runtime_error("cannot open file");
    }
    ~FileGuard() { if (file_) std::fclose(file_); }

    // A file owns a unique resource: copying would double-close. Forbid it.
    FileGuard(const FileGuard &) = delete;
    FileGuard &operator=(const FileGuard &) = delete;

    std::FILE *get() const { return file_; }

private:
    std::FILE *file_;
};

void writeReport() {
    FileGuard out("report.txt", "w");   // opened here
    std::fputs("ok\n", out.get());
    // even if an exception is thrown below, ~FileGuard closes the file
}

Smart pointers (unique_ptr, shared_ptr) are RAII for memory. The same idea generalises to every resource, which is why "rule of zero" classes need no destructor at all — their members already manage themselves.

Error handling strategy

There is no single "correct" way to report failure — there is a consistent way. Pick one strategy per boundary of your system and apply it uniformly, converting at the edges. Mixing exceptions and error codes at random is what makes error handling unreadable.

MechanismBest forCost / caution
Exceptionsrare, truly exceptional failures a caller cannot ignoreunwinding cost; keep them out of hot loops and destructors
std::optional<T>"value or nothing" where absence is normalcarries no error reason
std::error_codelibrary-level, non-throwing error reportingeasy to ignore if the caller does not check
std::expected<T,E> (C++23)"value or a typed error" without exceptionsnewer standard; check availability

Regardless of mechanism, guarantee the basic exception-safety guarantee: if a function throws, the program is still valid and no resource leaks — which RAII gives you for free. Mark the functions that never throw noexcept; the compiler optimises around that promise and will terminate if you break it.

#include <optional>
#include <string>

// Absence is a normal outcome here, so return optional — no exception, no sentinel.
std::optional<int> findAge(const std::string &name) {
    if (name == "Ada") return 36;
    return std::nullopt;                // "no age known"
}

void use() {
    if (auto age = findAge("Ada"))
        std::cout << "age = " << *age << "\n";
    else
        std::cout << "unknown\n";
}

Logging & observability

A running service must explain itself. Structured logs with severity levels let operators filter noise from signal, and an RAII timing guard records how long each unit of work took — automatically, even on early returns. The demo collection includes a working logger with a ScopedTiming guard.

enum class Level { Debug, Info, Warning, Error };

class Logger {
public:
    explicit Logger(Level minLevel) : minLevel_(minLevel) {}
    void log(Level level, const std::string &message) const {
        if (level < minLevel_) return;              // filter below the threshold
        std::cout << "[" << toTag(level) << "] " << message << "\n";
    }
private:
    Level minLevel_;
};

Good logs are event-oriented ("order 1024 committed"), not narration ("now we do X"). Never log secrets, and prefer a logging interface you can inject — so tests can assert on what was logged.

Design patterns

Patterns are a shared vocabulary for common structures, not a checklist to apply everywhere. A few recur constantly in C++ services:

  • Observer / Event bus — decouple producers from consumers with callbacks; see demo 14.
  • Factory — build objects through one function so the concrete type stays hidden.
  • Pimpl — hide implementation behind a pointer to keep the public header stable and rebuilds fast.
  • Strategy — swap an algorithm at run time by storing a std::function.
#include <functional>

// Strategy: the pricing rule is data, injectable and replaceable at run time.
class Checkout {
public:
    using Pricing = std::function<double(double)>;
    explicit Checkout(Pricing pricing) : pricing_(std::move(pricing)) {}
    double total(double subtotal) const { return pricing_(subtotal); }
private:
    Pricing pricing_;
};
// Checkout plain([](double s){ return s; });
// Checkout discount([](double s){ return s * 0.9; });

Build & dependencies

If a build is not reproducible, it is not enterprise. Modern C++ uses CMake with a target-based model: each library is a target with explicit PUBLIC/PRIVATE usage requirements. Third-party code comes from a package manager (vcpkg or Conan) with pinned versions, never from a random checkout.

cmake_minimum_required(VERSION 3.20)
project(billing CXX)                    # C++ project named "billing"

add_library(core src/core.cpp)          # a reusable library target
target_compile_features(core PUBLIC cxx_std_17)
target_include_directories(core PUBLIC include)
target_compile_options(core PRIVATE -Wall -Wextra -Wpedantic)   # warnings as policy

add_executable(billing_app src/main.cpp)
target_link_libraries(billing_app PRIVATE core)                 # explicit dependency

Warnings are not optional: -Wall -Wextra -Wpedantic (or MSVC /W4) catch real defects before they ship. Treat warnings as errors in CI so nobody can merge new ones.

Testing & CI

Automated tests are what let you change code without fear. Start with a framework such as Catch2 or GoogleTest, and run the same suite in continuous integration on every push. Because C++ lets you shoot yourself in the foot, layer dynamic analysers on top.

#define CATCH_CONFIG_MAIN       // or GoogleTest's main
#include <catch2/catch.hpp>

std::optional<int> findAge(const std::string &name);   // the function under test

TEST_CASE("findAge reports known and unknown names") {
    REQUIRE(findAge("Ada").value() == 36);
    REQUIRE(!findAge("Nobody").has_value());            // absence is a valid result
}
ToolCatchesTypical flag / command
AddressSanitizerbuffer overflows, use-after-free-fsanitize=address
UndefinedBehaviorSanitizerinteger overflow, bad casts-fsanitize=undefined
ThreadSanitizerdata races-fsanitize=thread
clang-tidystyle and bug-prone patternsclang-tidy src/*.cpp
valgrindleaks (Linux/macOS)valgrind ./app

Run sanitizer builds in CI as a separate, slower job. A sanitizer build that is green is far more convincing than any number of manual tests.

Practice

  1. Take one class you wrote and make it RAII-safe: no raw new, no manual cleanup.
  2. Give a function two return styles — one throwing, one returning std::optional — and argue which boundary each belongs to.
  3. Write a one-file CMake project that builds a library plus an executable and enables -Wall -Wextra.
  4. Add three TEST_CASEs for a function you wrote earlier and run them.
  5. Compile a demo with -fsanitize=address,undefined and fix anything it reports.