Refactoring is secretly inlining

By Andy Soffer, published April 26th, 2025

I'm excited to share a refactoring tool for C++ that I've been working on for the past month or so. In its simplest form, it inlines functions and type aliases of your choosing everywhere they're used. There is a demo you can play around with on Compiler Explorer.

As an example, suppose we annotated our PartsNeeded function with BRONTO_INLINE.

BRONTO_INLINE()
int PartsNeeded(int num_widgets) {
  return WidgetParts(num_widgets) + GadgetParts(0);
}

Everywhere PartsNeeded is called, our tool will replace it with the sum of calls to WidgetParts and GadgetParts.

 int TotalPartsNeeded(int widgets, int redundancy) {
-  return PartsNeeded(widgets) * redundancy;
+  return (WidgetParts(widgets) + GadgetParts(0)) * redundancy;
 }

Type aliases work similarly.

template <typename T>
using ConstRef BRONTO_INLINE() = const T&;

Running our tool with the above annotation produces diffs like this one

-   void MyFunction(ConstRef<std::string> something,
-                   ConstRef<const std::string> something_else) {
+   void MyFunction(const std::string& something,
+                   const std::string& something_else) {

So what?

The first time I heard about inlining as a refactoring mechasim, I wasn't terribly convinced. Since then, I have completely changed my tune. The rest of this blog post explains what changed my mind.

Martin Fowler's refactorings generally come in two kinds. What I call local refactorings, such as Split Loop, can be done in your text editor by editing a single file. They have no widespread effects. Everything you need to modify is right there inside a single function. Non-local refactorings, on the other hand, require updating multiple files. For example, Change Function Declaration requires updating all of the function's callers. Non-local refactorings are the tricky/expensive ones, especially when they need to update hundreds, thousands, or tens of thousands of files. Inlining is an incredibly powerful primitive that can trivially automate most non-local refactorings.

Your desired refactor may not be an inlining itself, but the key idea is that you can almost always tweak your code just a little bit to turn your refactoring problem into an inlining problem. Stated differently, if you have a tool that inlines things in the background automatically, non-local refactoring problems frequently reduce to just a few local edits.

Here are a few examples of how inlining solves other refactoring problems. Each heading is also a link to a working example on Compiler Explorer.

Rename a function

To rename a function, simply leave a version with the old name that calls the new one and ask BRONTO_INLINE to move over all the callers.

BRONTO_INLINE()
void OldFunctionName(int argument) {
  return NewFunctionName(argument);
}

Add a default argument to every function call-site

To add a default argument, create an overload that takes the desired extra argument and have the original call the new overload with the default you want, then have BRONTO_INLINE migrate all the callers.

// Overload of `MyFunction` that takes an integer `argument` which should
// default to 17.
void MyFunction(int argument, const std:string& s);

BRONTO_INLINE()
void MyFunction(const std::string& s) {
  return MyFunction(17, s);
}

Remove an unused parameter

Removing an unused parameter is just like adding a default parameter. Add an overload without the parameter, and have the original call the new version. The resulting inlining will will remove the parameter from every call-site automatically.

// Overload of `MyFunction` that doesn't take an integer `argument`
void MyFunction(const std:string& s);

BRONTO_INLINE()
void MyFunction(int argument, const std::string& s) {
  return MyFunction(s);
}

If you're wondering what happens when the expression passed in to argument has side-effects, stay tuned. I'm working on another post with this sort of nitty-gritty detail. The short of it is we thought about this case and have some smarts to make sure we don't remove expressions with side-effects.

Rename a type

Type-renaming works just like function-renaming. You can reimplement your type with the new name, leaving behind an alias of the old name referring to the new one, and then inline the alias. Just like functions, you can also add, remove, or rearrange template arguments.

using OldTypeName BRONTO_INLINE() = NewTypeName;

Convert between free functions and member functions

Converting between free functions and member functions is just a fancy way to rename a function.

// Method to function
void TargetFreeFunction(MyClass const& c);

class MyClass {
 public:
   BRONTO_INLINE()
   void should_be_free() const { return TargetFreeFunction(*this); }
};

// Function to method
struct MyStruct {
   void target_member();
};
BRONTO_INLINE()
void ShouldBeMember(MyStruct const& s) {
  return s.target_member();
}

Extract a specialization from a function template

You can split a function template into multiple separate declarations: each to be given the BRONTO_INLINE attribute or not according to your needs. For example, suppose we had a function template PrintInteger that printed any signed integral type, but we wanted the bit-width to be explicit. We would first define our new desired functions PrintInt8, PrintInt16, PrintInt32, and PrintInt64. Next, we would separate our template into four disjoint function templates, each one dispatching to one of these PrintInt* functions. Each of these four templates could then be separately annotated and inlined.

void PrintInt8(int8_t);
void PrintInt16(int16_t);
void PrintInt32(int32_t);
void PrintInt64(int64_t);

template <std::signed_integral T>
requires (sizeof(T) == 1)
BRONTO_INLINE()
void PrintInteger(T num) { PrintInt8(num); }

template <std::signed_integral T>
requires (sizeof(T) == 2)
BRONTO_INLINE()
void PrintInteger(T num) { PrintInt16(num); }

template <std::signed_integral T>
requires (sizeof(T) == 4)
BRONTO_INLINE()
void PrintInteger(T num) { PrintInt32(num); }

template <std::signed_integral T>
requires (sizeof(T) == 8)
BRONTO_INLINE()
void PrintInteger(T num) { PrintInt64(num); }

Is this really safe?

This is all well and good, but what about implicit conversions, order of operations, lifetime extension, namespacing issues, argument-dependent lookup, or hundreds of other C++ complexities? Surely just replacing the text doesn't work? No, in fact it doesn't. Even a rename can get hairy.

int Func();

namespace my_namespace {
int Func();

BRONTO_INLINE()
int OldFunction() { return 1 + Func(); }
}  // namespace my_namespace

In this example we can't just replace callers with the text of the function, because outside of my_namespace, the symbol Func will resolve to a different function than the one we intended to call. So when we talk about "textually inlining," it's an oversimplification. The tool takes care of many subtleties, including namespace resolution, so that the inlining is correct.

In the initial examples of this blog post, we slipped in two more cases where textual replacement isn't exactly what is happenning. In the first code snippet when we inlined PartsNeeded into TotalPartsNeeded, parentheses are needed to respect order of operations. Fortunately, BRONTO_INLINE is smart and adds parentheses precisely where they're necessary. In the type-alias example, we had an instance of ConstRef<const std::string>, which would naively inline as const const std::string&, but BRONTO_INLINE properly collapses the redundant const.

I had a lot of fun thinking through all the ways pure textual replacement could go wrong and addressing them. There are a fascinatingly large number of such cases to consider that I'll leave for a future blog post.

Conclusion

I have only included a smattering of what's possible with an automated inlining tool. It's not a silver bullet, but it covers a significant portion of practical refactorings automatically. I took the time to categorize the refactorings in Martin Fowler's catalogue. Slightly less than half of them were purely local refactorings (simplifying conditionals, converting loops into pipelines, etc). Over half of the remainder could be be implemented with inlinings. This is not to say the remaining quarter aren't valuable too, but it amazed me that one tool can automate so many of the most expensive refactoring use-cases.

As mentioned above, you can play around with the demo on Compiler Explorer. If you're interested in learning more or hearing about new tools and features as we build them, subscribe to our low-traffic announcement list. If you've got questions, requests, or suggestions, send us a shout out in the contact form below.


About the author

Andy Soffer

Andy Soffer

CTO, BrontoSource

Andy spent the past eight years at Google where he led the C++ Refactoring team as a Staff Engineer. During that time he designed and implemented novel technologies and techniques for source-to-source migrations across Google’s monorepo, as well as executing those migrations over hundreds of millions of lines of code.

Contact Us

If you are interested in learning more, partnering with us as an early customer, or investing, please reach out!
You can also subscribe to our announcement list to receive updates about what we are doing.