<p>In the C++ community, there is a well know compilation cascade problem, that can be solved with the pimpl ideom <a href="https://herbsutter.com/gotw/_100/" rel="noreferrer">https://herbsutter.com/gotw/_100/</a></p>
<p>Here's the problem: If you nest structs (with each struct being in a separate compilation unit), a change to a private member in the most deeply nested struct will cause all compilation units to recompile, leading to poor incremental compilation times.</p>
<p>You can introduce a <em>compilation fire wall</em> by predeclaring the struct <code>struct T;</code> and then holding a pointer in the member variable <code>T* p_member</code>, instead of the actual struct <code>T m_member</code>. This indirection has a runtime penalty.</p>
<p>Languages like Java and C# do this automatically. If you change a private member you only need to recompile just that class.</p>
<p>The runtime penalty can be got around in c++ with placement new pimpl (shown below), but still requires lots of boilerplate code and is a footgun for UB.</p>
<p>Rust appears to have no support for compilation firewalls <a href="https://internals.rust-lang.org/t/request-for-an-rfc-pimpl-for-rust/10947" rel="noreferrer">https://internals.rust-lang.org/t/request-for-an-rfc-pimpl-for-rust/10947</a>.</p>
<p><em>Why don't systems programming languages provide language features to do this automatically?</em> It seem like language designers do not care about incremental compilation times? Am I missing something?</p>
<pre><code>class steam_manager
{
private:
    struct impl;
    static constexpr std::size_t impl_size{ 10 };
    std::byte _buffer[impl_size];
    // Warning! UB here? Slides mentioned needing alignment
    impl* _impl() { return reinterpret_cast&lt;impl*&gt;(_buffer); }

public:
    steam_manager();
    ~steam_manager();
    void api0();
    void api1();
};
// steam_manager.cpp
struct steam_manager::impl
{
    unsigned private_member{12};
    void impl0() { std::cout &lt;&lt; private_member; }
    void impl1() { /* ... */ }
};
steam_manager::steam_manager()
{
    new (_buffer) impl();
    static_assert(sizeof(steam_manager::impl) &lt;= steam_manager::impl_size,
        &quot;impl size exceeds buffer size&quot;);
}
steam_manager::~steam_manager() = default;
void steam_manager::api0() { _impl()-&gt;impl0(); }
void steam_manager::api1() { _impl()-&gt;impl1(); }
</code></pre>
<p>Code adapted from:
<a href="https://www.youtube.com/watch?v=PfHD3BsVsAM" rel="noreferrer">https://www.youtube.com/watch?v=PfHD3BsVsAM</a>
<a href="https://github.com/vittorioromeo/accu2023/blob/main/improving_compilation_times.pdf" rel="noreferrer">https://github.com/vittorioromeo/accu2023/blob/main/improving_compilation_times.pdf</a></p>
<p>Without compilation firewalls, private members seem like a really unnecessary language feature tbh.</p>

<rust><c++>
Why don't languages do automatic pimpl?
CC BY-SA 4.0
2024-05-19T03:58:23.593Z
Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking.
Community Compiler
CC BY-SA 4.0
2024-05-19T04:11:43.093Z
Feel free to close. I was trying to ask about what languages do for compilation firewalls. It seems they just don't bother, and was wondering why.
Tom Huntington
CC BY-SA 4.0
2024-05-19T04:23:54.487Z
I just learned about the c++ idiom "pimpl". According to https://en.cppreference.com/w/cpp/language/pimpl the problem solved by pimpl is that in c++, code that uses a class needs to be to recompiled if the class's size, layout or private members change. Many languages don't have that problem to begin with, e.g. dynamic languages, or also things like Java if I understand right.
Eldritch Conundrum
CC BY-SA 4.0
2024-05-19T04:29:08.150Z
Swift has the concept of frozen vs resilient types in ABI-stable libraries; does that count? Frozen types are guaranteed to never change their layout, while resilient types may and are therefore almost always passed by reference under the hood.
Bbrk24
CC BY-SA 4.0
2024-05-19T04:29:35.777Z
@EldritchConundrum yes, the question is tagged `c++`  and `rust`. As mentioned java, c# and go will use pimpl using interfaces to break compilation dependencies of private members. But that requires an indirection, run-time slow down
Tom Huntington
CC BY-SA 4.0
2024-05-19T04:29:47.997Z
@Bbrk24 thanks I'll look into it
Tom Huntington
CC BY-SA 4.0
2024-05-19T04:57:27.607Z
It’s definitely a little unclear to me what precisely you’re asking about here. The meat of your question appears to be the sentence “What examples of compilation firewalls are there in language design?” but that is awfully broad! It seems like, from the comments, you are interested in solutions that explicitly *don’t* rely on a pointer indirection? If that’s the case, it would be very good to make that explicit in your question!
Alexis King
CC BY-SA 4.0
2024-05-19T13:47:11.730Z
"A lot of time is spent instantiating private members with types from the standard library templates" - I don't see how that relates to pimpl. I also can't guess what you mean by "compilation firewall", or why the question is tagged as having anything to do with Rust.
Karl Knechtel
CC BY-SA 4.0
2024-05-19T20:25:12.840Z
@KarlKnechtel Long ago in https://herbsutter.com/gotw/_100/ Herb Sutter also called the pimpl idiom a "compilation firewall" because of how it reduces compilation dependencies. Most languages don't have this problem because they use a pointer. But it seems this question is mostly not about pimpl itself, but about *placement new* pimpl, a variation that OP calls "simple" and Sutter calls "evil", that eliminates the pointer indirection, at a cost. I suppose that OP is imagining a world in which the boilerplate of implementing placement new pimpl could be reduced enough that the STL would use it.
Eldritch Conundrum
CC BY-SA 4.0
2024-05-20T02:37:28.540Z
@EldritchConundrum where does Sutter call it evil?
Tom Huntington
CC BY-SA 4.0
2024-05-21T01:28:31.687Z
@TomHuntington in Exceptional C++ and in http://www.gotw.ca/publications/mill05.htm under "Reckless Fixes and Optimizations, and Why They're Evil", Sutter calls it "unsafe", "evil", "dastardly", "pernicious", and "depraved". (I'm not making this up)
Eldritch Conundrum
CC BY-SA 4.0
2024-05-21T10:10:13.590Z
@EldritchConundrum Thanks! No, I know Sutter would say that. I was interested in reading why
Tom Huntington
CC BY-SA 4.0
2024-06-26T13:19:28.270Z
A lot of design choices in system languages are for maximum performance or to give the programmer maximum control, regardless of usability or compilation times or anything else.
CoffeeTableEspresso
<p>The problem solved by the (placement-new or standard) pimpl idiom is really specific to C++, where complex header files are <code>#include</code>d and compiled repeatedly.</p>
<ul>
<li>C uses the same <code>#include</code> mechanism as C++, and in theory every <code>struct</code> declared in a header file has the same problem as the <code>class</code> in your example. However, it is less of an issue because types in C tend to be much simpler than <code>std::map</code> etc. in C++.</li>
<li>More modern languages (including Rust, which you mention) have module systems that make the necessary information available at all usage sites after compiling the declaration <em>once</em>. As you observed, this information typically includes size and alignment, but the compiler may also need to know how to drop, copy, and/or move the object. (Your example currently invokes Undefined Behavior when copying or moving.)</li>
</ul>
<p>And in fact, two such solutions do exist for C++:</p>
<ul>
<li>Some compilers offer <a href="https://en.wikipedia.org/wiki/Precompiled_header" rel="nofollow noreferrer">precompiled headers</a>, to avoid compiling the same header files over and over again.</li>
<li><a href="https://en.cppreference.com/w/cpp/language/modules" rel="nofollow noreferrer">Modules</a> are in the standard since C++20, but have not seen widespread use yet. In a sense, they provide exactly the &quot;automatic placement-new pimpl&quot; you ask for (depending on how they are implemented, of course).</li>
</ul>
<p>Finally, note that this problem is not the only factor that affects compilation performance. For example, Rust compile times are comparable to C++ due to <a href="https://rustc-dev-guide.rust-lang.org/backend/monomorph.html" rel="nofollow noreferrer">monomorphization</a>, which is mostly an independent concern but a bit related: One variation of the pimpl idiom in C++ is to have a template class with a template-free pimpl, so that the template-free part is compiled only once. This, however, is not quite as mechanical as your example, since you need to separate the code into a template-based and a template-free part first.</p>

CC BY-SA 4.0
2024-05-19T12:46:24.883Z
2024-05-19T12:46:24.883Z
Sebastian Reichelt
CC BY-SA 4.0
2024-05-20T02:42:45.227Z
c++ modules might allow you to avoid indirection if you to write all public member functions as free functions i.e. `void func(T* self, ...)`. However, for member call `.` syntax you need to have the class publicly defined.
Tom Huntington
CC BY-SA 4.0
2024-05-20T02:54:23.827Z
rust still has the compilation cascade problem afaik. If you change a private member in a public struct in Rust, the modules that import this struct will need to recompile. Even if the layout of the public members didn't change.
Tom Huntington
CC BY-SA 4.0
2024-05-20T10:31:27.197Z
@TomHuntington, classes can be exported from C++ modules as well. (It's required for the standard library after all.)

Changing a private member can affect size and alignment, so in general everything that depends on the class/struct has to be recompiled, yes. But that is true even when using the pimpl idiom. If size and alignment haven't changed, the recompilation could probably be skipped (and maybe it actually is?). That is a compiler implementation issue, not a language design issue then.
Sebastian Reichelt
<h1>Pointer indirection is slow</h1>
<p>The name &quot;pimpl&quot; starts with a &quot;p&quot; that stands for &quot;pointer&quot;. Unfortunately, accessing the members of an object via a pointer can be slow, because the pointer first needs to be dereferenced, and this can lead to pipeline stalls if the CPU cannot predict ahead of time what address the pointer is pointing to. CPUs have gotten a bit better at this in recent times, so perhaps a single layer of indirection isn't so bad, but consider that more complex classes can contain member variables that are complex classes themselves, and suddenly what looks like a simple access to a member variable will result in multiple chained indirections.</p>
<p>Systems languages are focussed on runtime performance for good reasons, and therefore will avoid using pointers behind the scenes.</p>

CC BY-SA 4.0
2024-05-20T13:52:20.177Z
2024-05-20T13:52:20.177Z
G. Sliepen
CC BY-SA 4.0
2024-05-20T21:58:02.717Z
You can do pimpl without the indirection (assume the compiler optimizes it). Look at the code in the op. `std::byte _buffer[impl_size];` and `new (_buffer) impl();`
Tom Huntington
CC BY-SA 4.0
2024-05-20T22:03:00.020Z
Sure but how does it know the `impl_size`? If it is known at compile time, then you'd have to recompile all classes that have an impl when the impl type changes size. If you want to avoid recompiling a lot of code, then you can't use that method, and you need a pointer to a heap-allocated object.
G. Sliepen
CC BY-SA 4.0
2024-05-20T22:09:33.293Z
Changes in `impl_size` could be amortized (like `std::vector`'s allocation) between successive debug builds. The exact size could be used for release builds
Tom Huntington
CC BY-SA 4.0
2024-05-21T05:25:28.247Z
This sounds like it precludes linking seperately compiled object files. Also, it will waste space, which is important if you have an array of objects. So while that can be done, I think it's ultimately not practical.
G. Sliepen
CC BY-SA 4.0
2024-05-21T13:08:18.033Z
@G.Sliepen: I've actually implemented an inline PIMPL with rounded size, and it works quite beautifully. Yes, there's a slight overhead due to rounding, but given how big the PIMPL'ed objects typically are, it's a non-problem.
Matthieu M.
CC BY-SA 4.0
2024-07-22T16:55:59.950Z
@G.Sliepen: Some link systems may be able to have a module declare a "bss" section whose size is defined in terms of absolute link symbols.  Alternatively, if compilers output information about all  of the objects they needed of various types, as well as information about the sizes of types, a simple pre-link step could build an assembly language source file that would define and export symbols of all the appropriate sizes.
supercat
<p>First of all I have to apologize that I didn't know what &quot;pimpl&quot; is until recently, so my understanding of &quot;pimpl&quot; (and my answer) may be wrong.</p>
<blockquote>
<p>Languages like Java and C# do this automatically.</p>
</blockquote>
<p>Not really. The way these languages work internally is completely different from what &quot;pimpl&quot; does.</p>
<p>I assume that a normal Java or .NET (C#) JIT compiler will compile the following class:</p>
<pre><code>class childClass extends parentClass {
    public int example2;
    public int testMethod() { return super.example1 + example2; }
};
</code></pre>
<p>... to some x86 assembly code that looks <em>similar</em> to this one:</p>
<pre><code>.global sizeof_childClass
sizeof.childClass = 12 + sizeof.parentClass

.global offsetof.childClass.example2
offsetof.childClass.example2 = 4 + sizeof.parentClass

.global childClass.testMethod
childClass.testMethod:
    mov edx, [esp + 4]
    mov eax, [edx + offsetof.parentClass.example1]
    add eax, [edx + offsetof.childClass.example2]
    ret
</code></pre>
<p>Doing it like this has neither runtime nor memory consumption penalty.</p>
<blockquote>
<p>Why don't system programming languages do automatic pimpl?</p>
</blockquote>
<p>It depends on what you call &quot;<em>automatic pimpl</em>&quot;:</p>
<p>(1) If you want to develop a <strong>C++ compiler that takes <em>existing</em> C++ code</strong> and performs &quot;pimpl&quot;, this would not be so easy:</p>
<p>A C++ compiler re-compiles a .CPP file whenever a .H file is changed - even if only a comment is changed. Your compiler would need to check if the file content of the header files has changed instead - which is much more difficult.</p>
<p>(2) If you want to develop another <strong>programming language that automatically uses pointer indirection</strong> like &quot;pimpl&quot; does it:</p>
<p>As already written in the other answers, this would make the resulting code slower and you normally would always prefer better runtime performance over faster compiling speed.</p>
<p>(3) If you want to develop a new low-level <strong>programming language that internally works like Java</strong> (or better: the way I suspected that Java JIT compilers work):</p>
<p>This would be possible.</p>
<p>However, your programming language could not use the &quot;standard&quot; tool chains - especially not linkers - because the following line of assembly code (shown in the example above) could not be handled by &quot;normal&quot; linkers:</p>
<pre><code>sizeof.childClass = 12 + sizeof.parentClass
</code></pre>
<p>You would require a special linker but I assume that you want to use &quot;standard&quot; linkers for compatibility reasons.</p>

CC BY-SA 4.0
2024-06-28T06:31:27.667Z
2024-07-08T15:31:08.203Z
2024-07-08T15:31:08.203Z
Martin Rosenau
CC BY-SA 4.0
2024-06-28T08:34:31.933Z
Java/C#/Go do automatic pimpl. The question was why do systems programming languages not automate "placement new pimpl"? The real answer is they don't really care about compilation speed
Tom Huntington
CC BY-SA 4.0
2024-07-08T15:32:17.587Z
@TomHuntington I updated my answer - I hope it is better understandable now.
Martin Rosenau
<p>Swift actually does automatic compilation firewalls when importing from dynamic modules.</p>
<p>Its part of their automatic ABI stability.</p>
<p>They do dynamic stack allocations with <a href="https://llvm.org/docs/LangRef.html#alloca-instruction" rel="nofollow noreferrer">alloca</a> for structs imported from dynamic modules. This means they only need to know the size of the struct at runtime.</p>
<p>Here's the blog post that explain it to me <a href="https://faultlore.com/blah/swift-abi/" rel="nofollow noreferrer">https://faultlore.com/blah/swift-abi/</a></p>
<p><a href="https://github.com/fastred/Optimizing-Swift-Build-Times#modularization" rel="nofollow noreferrer">https://github.com/fastred/Optimizing-Swift-Build-Times#modularization</a> recommends splitting your code into modules to improve incremental compile times</p>

CC BY-SA 4.0
2024-06-26T06:13:04.593Z
2024-06-26T06:35:47.480Z
2024-06-26T06:35:47.480Z
Tom Huntington
<p><strong>TL;DR: Fast compilation or fast runtime, pick one.</strong></p>
<h3>PIMPL is not the answer</h3>
<p>A data-member being private only prevents <em>naming</em> the data-member in the source code. It does not preclude the compiler from relying on its (known) byte offset in the structure to access it -- which happens commonly when inlining<sup>1</sup> is involved.</p>
<p>You have failed to mention <em>Swift</em> in your question. PIMPL is extensively used for ABI stability purposes in C and C++, perhaps more often so than as a compilation firewall. The Swift programming language has extensive ABI stability guarantee tuning. Why did Swift create such an extensive ABI stability guarantee system rather than just PIMPL all the things? Because there's a runtime cost to it, and only the user knows whether the cost is acceptable or not.</p>
<p>As for the performance of Debug builds, read on.</p>
<p><sup>1</sup> <em>Note that even separate object compilation does not preclude inlining in the presence of link-time optimization.</em></p>
<h3>Better Automatic Firewalls exist</h3>
<p>Compilation time issues are mostly an issue of using antiquated compiler architectures, especially so now that C++ has (in theory) modules.</p>
<p>The rustc compiler has a much more modern architecture than Clang or GCC. It uses a query system -- see Adapton -- which automatically include compilation firewalls at every step. In short:</p>
<ol>
<li>Each &quot;pass&quot; of the compiler is a &quot;query&quot;, producing one (or several) output(s); anything the query &quot;uses&quot; during its worked is included in the dependency set of its output(s).</li>
<li>When executing a query to produce an output which has already been produced previously, each dependency is checked. If no dependency has changed, the previous output is reused.</li>
</ol>
<p>This has far-reaching implications, but quick examples include:</p>
<ol>
<li>Flipping two letters in a comment: after syntactic analysis, the AST is the same as the previous AST, all previous outputs produced from this AST need not change: no need to do semantic analysis, code generation, debug info generation, etc...</li>
<li>Flipping two letters of a private data-member identifier: after semantic analysis, the SSA code is the same, so no code need be generated, but debug info need be updated (if enabled).</li>
</ol>
<p>The problem that rustc faces, is that it uses LLVM, and LLVM doesn't do incremental compilation. So any time <em>anything</em> changes that requires involving LLVM, a whole bunch of unnecessary work is performed again by LLVM. It mostly matters because LLVM (and preparing LLVM IR to pass to LLVM) is not that fast.</p>
<p>A partial workaround for Debug is that rustc splits the code of any library or binary in 256 LLVM modules by default (in Debug), so an isolated change typically only requires updating (and invoking LLVM on) a handful of LLVM modules at a time... but that's not ideal, obviously.</p>
<hr />
<blockquote>
<p>Without compilation firewalls, private members seem like a really unnecessary language feature tbh.</p>
</blockquote>
<p>Private members are about <em>encapsulation</em> and <em>invariants</em>: ie maintainability and correctness. They fulfill their purpose, admirably.</p>

CC BY-SA 4.0
2024-05-21T13:37:03.483Z
2024-05-21T13:37:03.483Z
Matthieu M.
CC BY-SA 4.0
2024-05-21T21:49:15.427Z
Amortizing changes in `impl_size`  across successive debug builds would reduce the dependencies between the LLVM modules. https://oxide.computer/podcasts/oxide-and-friends/1701673 recommended always using `dyn` to break the dependencies despite the runtime slowdown
Tom Huntington
CC BY-SA 4.0
2024-05-22T07:25:23.153Z
@TomHuntington: It would reduce size changes, certainly, but because functions operating on the actual stored type may be strewn around multiple LLVM modules (and will need to change), you may still end up having multiple LLVM modules to recompile. Also, there are opportunity costs in having Debug != Release, representation-wise: you can't easily mix Debug & Release artifacts any longer, bugs may manifest in Release that you can't reproduce in Debug, etc...
Matthieu M.
# Why don't languages do automatic pimpl?
Posted by: username_0 on 2024-05-19
In the C++ community, there is a well know compilation cascade problem, that can be solved with the pimpl ideom https://herbsutter.com/gotw/_100/
Here's the problem: If you nest structs (with each struct being in a separate compilation unit), a change to a private member in the most deeply nested struct will cause all compilation units to recompile, leading to poor incremental compilation times.
You can introduce a compilation fire wall by predeclaring the struct struct T; and then holding a pointer in the member variable T* p_member, instead of the actual struct T m_member. This indirection has a runtime penalty.
Languages like Java and C# do this automatically. If you change a private member you only need to recompile just that class.
The runtime penalty can be got around in c++ with placement new pimpl (shown below), but still requires lots of boilerplate code and is a footgun for UB.
Rust appears to have no support for compilation firewalls https://internals.rust-lang.org/t/request-for-an-rfc-pimpl-for-rust/10947.
Why don't systems programming languages provide language features to do this automatically? It seem like language designers do not care about incremental compilation times? Am I missing something?
class steam_manager
{
private:
    struct impl;
    static constexpr std::size_t impl_size{ 10 };
    std::byte _buffer[impl_size];
    // Warning! UB here? Slides mentioned needing alignment
    impl* _impl() { return reinterpret_cast(_buffer); }

public:
    steam_manager();
    ~steam_manager();
    void api0();
    void api1();
};
// steam_manager.cpp
struct steam_manager::impl
{
    unsigned private_member{12};
    void impl0() { std::cout << private_member; }
    void impl1() { /* ... */ }
};
steam_manager::steam_manager()
{
    new (_buffer) impl();
    static_assert(sizeof(steam_manager::impl) <= steam_manager::impl_size,
        "impl size exceeds buffer size");
}
steam_manager::~steam_manager() = default;
void steam_manager::api0() { _impl()->impl0(); }
void steam_manager::api1() { _impl()->impl1(); }

Code adapted from:
https://www.youtube.com/watch?v=PfHD3BsVsAM
https://github.com/vittorioromeo/accu2023/blob/main/improving_compilation_times.pdf
Without compilation firewalls, private members seem like a really unnecessary language feature tbh.
**Tags:** rust c++
> **Comments:**
> **username_1**: Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking.  
> **username_0**: Feel free to close. I was trying to ask about what languages do for compilation firewalls. It seems they just don't bother, and was wondering why.  
> **username_2**: I just learned about the c++ idiom "pimpl". According to https://en.cppreference.com/w/cpp/language/pimpl the problem solved by pimpl is that in c++, code that uses a class needs to be to recompiled if the class's size, layout or private members change. Many languages don't have that problem to begin with, e.g. dynamic languages, or also things like Java if I understand right.  
> **username_3**: Swift has the concept of frozen vs resilient types in ABI-stable libraries; does that count? Frozen types are guaranteed to never change their layout, while resilient types may and are therefore almost always passed by reference under the hood.  
> **username_0**: @EldritchConundrum yes, the question is tagged `c++`  and `rust`. As mentioned java, c# and go will use pimpl using interfaces to break compilation dependencies of private members. But that requires an indirection, run-time slow down  
> **username_0**: @Bbrk24 thanks I'll look into it  
> **username_4**: It’s definitely a little unclear to me what precisely you’re asking about here. The meat of your question appears to be the sentence “What examples of compilation firewalls are there in language design?” but that is awfully broad! It seems like, from the comments, you are interested in solutions that explicitly *don’t* rely on a pointer indirection? If that’s the case, it would be very good to make that explicit in your question!  
> **username_5**: "A lot of time is spent instantiating private members with types from the standard library templates" - I don't see how that relates to pimpl. I also can't guess what you mean by "compilation firewall", or why the question is tagged as having anything to do with Rust.  
> **username_2**: @KarlKnechtel Long ago in https://herbsutter.com/gotw/_100/ Herb Sutter also called the pimpl idiom a "compilation firewall" because of how it reduces compilation dependencies. Most languages don't have this problem because they use a pointer. But it seems this question is mostly not about pimpl itself, but about *placement new* pimpl, a variation that OP calls "simple" and Sutter calls "evil", that eliminates the pointer indirection, at a cost. I suppose that OP is imagining a world in which the boilerplate of implementing placement new pimpl could be reduced enough that the STL would use it.  
> **username_0**: @EldritchConundrum where does Sutter call it evil?  
> **username_2**: @TomHuntington in Exceptional C++ and in http://www.gotw.ca/publications/mill05.htm under "Reckless Fixes and Optimizations, and Why They're Evil", Sutter calls it "unsafe", "evil", "dastardly", "pernicious", and "depraved". (I'm not making this up)  
> **username_0**: @EldritchConundrum Thanks! No, I know Sutter would say that. I was interested in reading why  
> **username_6**: A lot of design choices in system languages are for maximum performance or to give the programmer maximum control, regardless of usability or compilation times or anything else.  
---
## 5 answers
### Answer by username_9 on 2024-05-21. Score: 8 | Accepted answer
**TL;DR: Fast compilation or fast runtime, pick one.**

### PIMPL is not the answer

A data-member being private only prevents *naming* the data-member in the source code. It does not preclude the compiler from relying on its (known) byte offset in the structure to access it -- which happens commonly when inlining1 is involved.

You have failed to mention *Swift* in your question. PIMPL is extensively used for ABI stability purposes in C and C++, perhaps more often so than as a compilation firewall. The Swift programming language has extensive ABI stability guarantee tuning. Why did Swift create such an extensive ABI stability guarantee system rather than just PIMPL all the things? Because there's a runtime cost to it, and only the user knows whether the cost is acceptable or not.

As for the performance of Debug builds, read on.

1 *Note that even separate object compilation does not preclude inlining in the presence of link-time optimization.*

### Better Automatic Firewalls exist

Compilation time issues are mostly an issue of using antiquated compiler architectures, especially so now that C++ has (in theory) modules.

The rustc compiler has a much more modern architecture than Clang or GCC. It uses a query system -- see Adapton -- which automatically include compilation firewalls at every step. In short:

1. Each "pass" of the compiler is a "query", producing one (or several) output(s); anything the query "uses" during its worked is included in the dependency set of its output(s).
2. When executing a query to produce an output which has already been produced previously, each dependency is checked. If no dependency has changed, the previous output is reused.

This has far-reaching implications, but quick examples include:

1. Flipping two letters in a comment: after syntactic analysis, the AST is the same as the previous AST, all previous outputs produced from this AST need not change: no need to do semantic analysis, code generation, debug info generation, etc...
2. Flipping two letters of a private data-member identifier: after semantic analysis, the SSA code is the same, so no code need be generated, but debug info need be updated (if enabled).

The problem that rustc faces, is that it uses LLVM, and LLVM doesn't do incremental compilation. So any time *anything* changes that requires involving LLVM, a whole bunch of unnecessary work is performed again by LLVM. It mostly matters because LLVM (and preparing LLVM IR to pass to LLVM) is not that fast.

A partial workaround for Debug is that rustc splits the code of any library or binary in 256 LLVM modules by default (in Debug), so an isolated change typically only requires updating (and invoking LLVM on) a handful of LLVM modules at a time... but that's not ideal, obviously.

---

> Without compilation firewalls, private members seem like a really unnecessary language feature tbh.

Private members are about *encapsulation* and *invariants*: ie maintainability and correctness. They fulfill their purpose, admirably.
> **Comments:**
> **username_0** (2024-05-21): Amortizing changes in `impl_size`  across successive debug builds would reduce the dependencies between the LLVM modules. https://oxide.computer/podcasts/oxide-and-friends/1701673 recommended always using `dyn` to break the dependencies despite the runtime slowdown  
> **username_9** (2024-05-22): @TomHuntington: It would reduce size changes, certainly, but because functions operating on the actual stored type may be strewn around multiple LLVM modules (and will need to change), you may still end up having multiple LLVM modules to recompile. Also, there are opportunity costs in having Debug != Release, representation-wise: you can't easily mix Debug & Release artifacts any longer, bugs may manifest in Release that you can't reproduce in Debug, etc...  
---
### Answer by username_7 on 2024-05-19. Score: 4
The problem solved by the (placement-new or standard) pimpl idiom is really specific to C++, where complex header files are `#include`d and compiled repeatedly.

* C uses the same `#include` mechanism as C++, and in theory every `struct` declared in a header file has the same problem as the `class` in your example. However, it is less of an issue because types in C tend to be much simpler than `std::map` etc. in C++.
* More modern languages (including Rust, which you mention) have module systems that make the necessary information available at all usage sites after compiling the declaration *once*. As you observed, this information typically includes size and alignment, but the compiler may also need to know how to drop, copy, and/or move the object. (Your example currently invokes Undefined Behavior when copying or moving.)

And in fact, two such solutions do exist for C++:

* Some compilers offer [precompiled headers](https://en.wikipedia.org/wiki/Precompiled_header), to avoid compiling the same header files over and over again.
* [Modules](https://en.cppreference.com/w/cpp/language/modules) are in the standard since C++20, but have not seen widespread use yet. In a sense, they provide exactly the "automatic placement-new pimpl" you ask for (depending on how they are implemented, of course).

Finally, note that this problem is not the only factor that affects compilation performance. For example, Rust compile times are comparable to C++ due to [monomorphization](https://rustc-dev-guide.rust-lang.org/backend/monomorph.html), which is mostly an independent concern but a bit related: One variation of the pimpl idiom in C++ is to have a template class with a template-free pimpl, so that the template-free part is compiled only once. This, however, is not quite as mechanical as your example, since you need to separate the code into a template-based and a template-free part first.
> **Comments:**
> **username_0** (2024-05-20): c++ modules might allow you to avoid indirection if you to write all public member functions as free functions i.e. `void func(T* self, ...)`. However, for member call `.` syntax you need to have the class publicly defined.  
> **username_0** (2024-05-20): rust still has the compilation cascade problem afaik. If you change a private member in a public struct in Rust, the modules that import this struct will need to recompile. Even if the layout of the public members didn't change.  
> **username_7** (2024-05-20): @TomHuntington, classes can be exported from C++ modules as well. (It's required for the standard library after all.)

Changing a private member can affect size and alignment, so in general everything that depends on the class/struct has to be recompiled, yes. But that is true even when using the pimpl idiom. If size and alignment haven't changed, the recompilation could probably be skipped (and maybe it actually is?). That is a compiler implementation issue, not a language design issue then.  
---
### Answer by username_8 on 2024-05-20. Score: 3
# Pointer indirection is slow

The name "pimpl" starts with a "p" that stands for "pointer". Unfortunately, accessing the members of an object via a pointer can be slow, because the pointer first needs to be dereferenced, and this can lead to pipeline stalls if the CPU cannot predict ahead of time what address the pointer is pointing to. CPUs have gotten a bit better at this in recent times, so perhaps a single layer of indirection isn't so bad, but consider that more complex classes can contain member variables that are complex classes themselves, and suddenly what looks like a simple access to a member variable will result in multiple chained indirections.

Systems languages are focussed on runtime performance for good reasons, and therefore will avoid using pointers behind the scenes.
> **Comments:**
> **username_0** (2024-05-20): You can do pimpl without the indirection (assume the compiler optimizes it). Look at the code in the op. `std::byte _buffer[impl_size];` and `new (_buffer) impl();`  
> **username_8** (2024-05-20): Sure but how does it know the `impl_size`? If it is known at compile time, then you'd have to recompile all classes that have an impl when the impl type changes size. If you want to avoid recompiling a lot of code, then you can't use that method, and you need a pointer to a heap-allocated object.  
> **username_0** (2024-05-20): Changes in `impl_size` could be amortized (like `std::vector`'s allocation) between successive debug builds. The exact size could be used for release builds  
> **username_8** (2024-05-21): This sounds like it precludes linking seperately compiled object files. Also, it will waste space, which is important if you have an array of objects. So while that can be done, I think it's ultimately not practical.  
> **username_9** (2024-05-21): @G.Sliepen: I've actually implemented an inline PIMPL with rounded size, and it works quite beautifully. Yes, there's a slight overhead due to rounding, but given how big the PIMPL'ed objects typically are, it's a non-problem.  
> **username_10** (2024-07-22): @G.Sliepen: Some link systems may be able to have a module declare a "bss" section whose size is defined in terms of absolute link symbols.  Alternatively, if compilers output information about all  of the objects they needed of various types, as well as information about the sizes of types, a simple pre-link step could build an assembly language source file that would define and export symbols of all the appropriate sizes.  
---
### Answer by username_11 on 2024-06-28. Score: 2
First of all I have to apologize that I didn't know what "pimpl" is until recently, so my understanding of "pimpl" (and my answer) may be wrong.

Languages like Java and C# do this automatically.

Not really. The way these languages work internally is completely different from what "pimpl" does.
I assume that a normal Java or .NET (C#) JIT compiler will compile the following class:
class childClass extends parentClass {
    public int example2;
    public int testMethod() { return super.example1 + example2; }
};

... to some x86 assembly code that looks similar to this one:
.global sizeof_childClass
sizeof.childClass = 12 + sizeof.parentClass

.global offsetof.childClass.example2
offsetof.childClass.example2 = 4 + sizeof.parentClass

.global childClass.testMethod
childClass.testMethod:
    mov edx, [esp + 4]
    mov eax, [edx + offsetof.parentClass.example1]
    add eax, [edx + offsetof.childClass.example2]
    ret

Doing it like this has neither runtime nor memory consumption penalty.

Why don't system programming languages do automatic pimpl?

It depends on what you call "automatic pimpl":
(1) If you want to develop a C++ compiler that takes existing C++ code and performs "pimpl", this would not be so easy:
A C++ compiler re-compiles a .CPP file whenever a .H file is changed - even if only a comment is changed. Your compiler would need to check if the file content of the header files has changed instead - which is much more difficult.
(2) If you want to develop another programming language that automatically uses pointer indirection like "pimpl" does it:
As already written in the other answers, this would make the resulting code slower and you normally would always prefer better runtime performance over faster compiling speed.
(3) If you want to develop a new low-level programming language that internally works like Java (or better: the way I suspected that Java JIT compilers work):
This would be possible.
However, your programming language could not use the "standard" tool chains - especially not linkers - because the following line of assembly code (shown in the example above) could not be handled by "normal" linkers:
sizeof.childClass = 12 + sizeof.parentClass

You would require a special linker but I assume that you want to use "standard" linkers for compatibility reasons.
> **Comments:**
> **username_0** (2024-06-28): Java/C#/Go do automatic pimpl. The question was why do systems programming languages not automate "placement new pimpl"? The real answer is they don't really care about compilation speed  
> **username_11** (2024-07-08): @TomHuntington I updated my answer - I hope it is better understandable now.  
---
### Answer by username_0 on 2024-06-26. Score: 0
Swift actually does automatic compilation firewalls when importing from dynamic modules.

Its part of their automatic ABI stability.

They do dynamic stack allocations with [alloca](https://llvm.org/docs/LangRef.html#alloca-instruction) for structs imported from dynamic modules. This means they only need to know the size of the struct at runtime.

Here's the blog post that explain it to me <https://faultlore.com/blah/swift-abi/>

<https://github.com/fastred/Optimizing-Swift-Build-Times#modularization> recommends splitting your code into modules to improve incremental compile times
---
