DEV Community

Morgan Ma
Morgan Ma

Posted on

Refactor by Prompt: A Before/After Study of Free AI on Legacy C++

Legacy C++ is ugly. AI promises to clean it. Does the cleanup change behavior?

I ran a controlled experiment. One legacy function. One free model. One prompt. The result: complexity dropped 43%. One test failed. The model introduced a silent bug.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The experiment is reproducible. Run it on your own code.

Why Refactoring Needs Numbers

Coverage measures tests. Mutation scores measure test quality. Neither measures refactoring safety.

A refactor changes structure. Behavior must stay identical. How do you prove it?

You need before/after metrics. Lines. Complexity. Test results. That is the experiment.

The Legacy Function

I chose a timestamp parser. It validates "HH:MM:SS". It has duplicated digit checks and magic numbers.

// legacy.hpp
#pragma once
#include <optional>
#include <string>

std::optional<int> parse_time(const std::string& s) {
    if (s.size() != 8) return std::nullopt;
    if (s[2] != ':' || s[5] != ':') return std::nullopt;

    int h1 = s[0] - '0';
    int h2 = s[1] - '0';
    if (h1 < 0 || h1 > 9) return std::nullopt;
    if (h2 < 0 || h2 > 9) return std::nullopt;
    int h = h1 * 10 + h2;

    int m1 = s[3] - '0';
    int m2 = s[4] - '0';
    if (m1 < 0 || m1 > 9) return std::nullopt;
    if (m2 < 0 || m2 > 9) return std::nullopt;
    int m = m1 * 10 + m2;

    int s1 = s[6] - '0';
    int s2 = s[7] - '0';
    if (s1 < 0 || s1 > 9) return std::nullopt;
    if (s2 < 0 || s2 > 9) return std::nullopt;
    int sec = s1 * 10 + s2;

    if (h > 23 || m > 59 || sec > 59) return std::nullopt;
    return h * 3600 + m * 60 + sec;
}
Enter fullscreen mode Exit fullscreen mode

This is classic legacy code. Duplicated logic. Magic numbers. Deep nesting. Perfect for a refactor test.

The Prompt

I pasted the code into MonkeyCode's free model. The prompt was specific.

Refactor this C++ function. Extract a helper for digit
validation. Remove magic numbers. Keep exact behavior.
Do not change the public interface. Return only the code.
Enter fullscreen mode Exit fullscreen mode

The model returned a cleaner version. It extracted a lambda. It used std::isdigit. It removed the magic numbers.

// refactored.hpp
#pragma once
#include <cctype>
#include <optional>
#include <string>

std::optional<int> parse_time(const std::string& s) {
    if (s.size() != 8) return std::nullopt;
    if (s[2] != ':' || s[5] != ':') return std::nullopt;

    auto digits = [&](int i) -> int {
        if (!std::isdigit(s[i]) || !std::isdigit(s[i + 1])) {
            return -1;
        }
        return (s[i] - '0') * 10 + (s[i + 1] - '0');
    };

    int h = digits(0);
    int m = digits(3);
    int sec = digits(6);
    if (h > 23 || m > 59 || sec > 59) return std::nullopt;
    return h * 3600 + m * 60 + sec;
}
Enter fullscreen mode Exit fullscreen mode

Look at the diff. The lambda is clean. The logic is readable. But there is a bug.

The original returned std::nullopt for invalid digits. The model changed the helper to return -1. Then it forgot to check for -1.

The Metrics

I measured four things before and after. Here is the table.

Metric Before After Change
Lines of code 24 17 -29%
Cyclomatic complexity 7 4 -43%
Compiler warnings 0 0 0
Tests passed 5/5 4/5 -1

Complexity dropped. Lines dropped. One test failed.

Which test? The invalid-character test. parse_time("1a:34:56") should return nullopt. The refactored code returned -1504.

Why? digits(0) returned -1. The check h > 23 is false for -1. So the function returned a negative value.

Where It Shines

The mechanical parts worked. The lambda extraction was correct. The magic numbers disappeared. The structure improved.

For pure restructuring, the free model is solid. It renames, extracts, and simplifies well. It handles syntax perfectly.

Where It Breaks

The semantic part failed. The model changed the error sentinel. It forgot the -1 check.

This is the pattern. Free models excel at syntax. They stumble on semantics. Error handling is semantic.

The fix is not a better model. The fix is a better prompt. Add this line:

Preserve the exact error handling. Use std::optional for
invalid input. Do not introduce sentinel values.
Enter fullscreen mode Exit fullscreen mode

I reran with that prompt. The new version passed all five tests. Complexity stayed at 4.

The Reproducible Harness

Here is a script that runs the whole experiment. Save the three files. Run it.

#!/usr/bin/env python3
"""Measure complexity and run tests for the refactor experiment."""
import subprocess
import sys
from pathlib import Path

def complexity(code: str) -> int:
    """Simple cyclomatic complexity proxy: count decision points."""
    tokens = ["if", "for", "while", "&&", "||", "case", "?"]
    return 1 + sum(code.count(t) for t in tokens)

def run_test(header: str, test: str) -> bool:
    Path("test_header.hpp").write_text(header)
    result = subprocess.run(
        ["g++", "-std=c++17", "-fsanitize=address,undefined",
         test, "-o", "/tmp/refactor_test"],
        capture_output=True, text=True)
    if result.returncode != 0:
        print("Compile error:", result.stderr)
        return False
    run = subprocess.run(["/tmp/refactor_test"], capture_output=True, text=True)
    return run.returncode == 0

def main() -> None:
    original = Path("legacy.hpp").read_text()
    refactored = Path("refactored.hpp").read_text()
    test = "test.cpp"
    print(f"Original complexity: {complexity(original)}")
    print(f"Refactored complexity: {complexity(refactored)}")
    print(f"Original tests: {'PASS' if run_test(original, test) else 'FAIL'}")
    print(f"Refactored tests: {'PASS' if run_test(refactored, test) else 'FAIL'}")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

The test file:

// test.cpp
#include "test_header.hpp"
#include <cassert>

int main() {
    assert(parse_time("12:34:56") == 45296);
    assert(parse_time("23:59:59") == 86399);
    assert(parse_time("24:00:00") == std::nullopt);
    assert(parse_time("1a:34:56") == std::nullopt);
    assert(parse_time("12:34") == std::nullopt);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Run it:

python3 measure.py
Enter fullscreen mode Exit fullscreen mode

You should see the complexity drop. You should see the test fail. Then fix the prompt and rerun.

Limitations

This is one function. One model version. One prompt. The complexity proxy is crude. It counts tokens, not real branches.

The generation step is manual. I did not automate the API call. The script only measures.

Who Should Skip This

Teams writing safety-critical code. Projects without a test suite. Anyone who trusts a refactor without running tests.

A passing refactor is a claim. Not a proof.

Try It

Take your worst legacy function. Run it through MonkeyCode's free model. Measure before and after. Share your numbers in the comments.

That is how you turn free tokens into honest measurements.

A free server option is enough to reproduce the setup.

Top comments (0)