Reducing C++ template bloat by factoring out the type-dependent portions

ingve1 pts0 comments

Skip to main content

Dev Blogs

AI

All .NET posts

.NET MAUI<br>ASP.NET Core<br>Blazor<br>Entity Framework

C++<br>C#<br>F#<br>TypeScript

NuGet<br>Servicing<br>.NET Blog in Chinese

Microsoft for Developers<br>Agent Framework<br>Develop from the cloud<br>Xcode<br>ISE Developer<br>TypeScript<br>PowerShell<br>Python<br>Java<br>Java Blog in Chinese<br>Go<br>Microsoft Edge Dev<br>Microsoft 365 Developer<br>Microsoft Entra Identity Developer<br>Microsoft Entra PowerShell

Visual Studio<br>Visual Studio Code<br>Aspire

All things Azure<br>Azure SDK<br>Azure VM Runtime Team<br>Microsoft Azure<br>Azure Cosmos DB<br>Azure DocumentDB<br>Azure Data Studio<br>Azure SQL<br>DevOps<br>DirectX<br>Microsoft Foundry<br>Power Platform

OData<br>Unified Data Model (IDEAs)

Windows Command Line<br>#ifdef Windows<br>Inside MSIX<br>MIDI and music<br>React Native<br>The Old New Thing<br>Windows Developer

Raymond Chen

C++ templates let you reuse code, but it comes at a cost: Each template expansion results in a different function. This is not a big deal for small functions, but the less trivial your function becomes, the larger the cost of the repeated expansions.

This is particularly expensive for functions that accept lambdas because every lambda is a unique type, so each time you invoke the template function with a lambda you get a different template expansion.

Sometimes I see large template functions that have very few type dependencies.

template<br>void something(Database const& db)<br>// extensive preparations<br>auto statusIndicator = ⟦ calculate status indicator ⟧<br>auto primaryTugboat = ⟦ calculate primary tugboat ⟧<br>std::vector staircases;

for (auto&& column : Table::Columns()) {<br>⟦ operate on each column using the stuff we prepared ⟧<br>⟦ maybe add things to the staircases and update the tugboat ⟧

⟦ lots more code ⟧

In this extreme case, the only type dependency is the Table::Columns(). (A more common source of type dependencies would be method calls on a templated inbound parameter.)

This is a large function, and it will be re-expanded for each Table. Since each table has a different set of columns, and probably a different number of columns, there is no opportunity for COMDAT folding, so the different expansions will all be distinct.

One way to mitigate the explosion is to wrap all the common pieces into a helper object.

struct SomethingState {<br>Database const& db;<br>Indicator statusIndicator;<br>Tugboat primaryTugboat;<br>std::vector staircases;

__declspec(noinline)<br>SomethingState(Database const& db) : db(db)<br>statusIndicator = ⟦ calulate status indicator ⟧<br>primaryTugboat = ⟦ calulate primary tugboat ⟧

__declspec(noinline)<br>void ProcessColumn(Column const& column)<br>⟦ operate on each column using the stuff we prepared ⟧<br>⟦ maybe add things to the staircases and update the tugboat ⟧

__declspec(noinline)<br>void Finish()<br>⟦ lots more code ⟧<br>};

template<br>void something(Database const& db)<br>SomethingState state(db);

for (auto&& column : Table::Columns()) {<br>state.ProcessColumn(column);

state.Finish();

Now, the different expansions of the something function can share the SomethingState constructor and methods, so the unique functions are fairly small.

We mark the SomethingState constructor and methods as "no-inline" to discourage the compiler from inlining them, because inlining them would defeat our factoring. Related : A noinline inline function? What sorcery is this?

Another way to reduce the code explosion problem is to do the factoring the other way: Instead of factoring out the common logic and keeping the type-dependent stuff, we factor out the type-dependent stuff and keep the common logic.

The trick with this approach is finding some common type that all of the expansions share. I’ll assume that the Table::Colums() is a C-style array of Column objects, or a std::vector of Column objects, or a std::array of Column objects, or otherwise something that can produce a std::span of Column objects.

void somethingWorker(Database const& db, std::span columns)<br>// extensive preparations<br>auto statusIndicator = ⟦ calculate status indicator ⟧<br>auto primaryTugboat = ⟦ calculate primary tugboat ⟧<br>std::vector staircases;

for (auto&& column : columns) {<br>⟦ operate on each column using the stuff we prepared ⟧<br>⟦ maybe add things to the staircases and update the tugboat ⟧

⟦ lots more code ⟧

template<br>void something(Database const& db)<br>somethingWorker(db, Table::Columns());

We capture the columns ahead of time and then use the captured values to perform the enumeration inside a non-templated worker function. Since the worker function is non-templated, there is no template explosion when it is called by each something.

One thing to watch out for is that we are changing the order of evaluation, The old code didn’t call Table::Columns() until after the preparations were complete. You can look at the code to confirm, but I suspect that Table::Columns() just returns a reference to some pre-existing source of column information, so it doesn’t matter when you call it. Even if it returned the columns by value (say, by cloning an internal vector), retrieving the columns early...

column columns template table type code

Related Articles