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.