Consider the following translation unit:
export module Example;
export inline void fn1();
export void fn2();
export void fn3();
void fn1() {}
void fn2() {}
module :private;
void fn3() {}
Firstly, I understand that fn1() works like a "traditional" inline function in a header file -- in particular, its definition is available for inlining in importing TUs.
Secondly, I gather from the example on cppreference that because it's in the private module fragment, the definition of fn3() is definitely not available to importing TUs (and therefore I'm free to change it without affecting ABI).
My question is about fn2(), which is defined in the module interface unit, but not marked with the inline keyword:
fn2()?
f1() and f2()?f2() and f3()?export module Example;
namespace {
void secret(int) {} // TU-local
inline void wrapper() {secret(0);} // OK
}
struct opaque; // incomplete
/*export*/ inline void fn1() {secret(42);} // error: exposure
/*export*/ void fn2() {secret(-1);} // OK
/*export*/ opaque *fn3();
module :private;
struct opaque {};
opaque *fn3() {return new opaque;} // OK
The changes here are
secret with internal linkage and use it in fn1 and fn2fn3 return a pointer to a new opaque typeexport (see below)fn1 vs. fn2An inline function (as well as a function template in many cases) can't use a TU-local entity like secret (unless it is itself TU-local like wrapper). The practical reason for this restriction is that it means that the compiler can assume that declarations whose effects have traditionally been restricted to one translation unit are still so restricted even if that translation unit is imported. In particular, no out-of-line copy of secret needs to be emitted if its (trivial) function body is successfully inlined into fn2: callers of fn1 (if we allowed it) might inline it and need their own reference to secret, but callers of the non-inline fn2 can be made to refer to just the symbol fn2. (Note that emitting it would involve choosing a globally unique name for the symbol, which is not trivial since its language-level name need not be unique.)
Note the implication of that implementation strategy: if functions are inlined only if they are declared inline (such that it is safe to do so with regard to TU-local things), a non-inline function serves as an ABI boundary. (In particular, for this sort of implementation changing the body of fn2 does not require recompiling clients, and secret might be removed entirely. Your build system might still react only to modification times, of course.)
Note that this restores in no small part the original meaning of inline: while it's no more a requirement than it has ever been, implementations have reason to take it as a necessary condition for actual assembly-level inlining. Removing the "hush the linker" meaning for inline also makes it entirely useless to declare a variable inline in the purview of a named module, although it hasn't been deprecated to do so.
Note that this does not depend on exportation in any way: the distinction is between "inline and not TU-local" (like fn1) and not (like wrapper or fn2). The reason for this is that (in the absence of a private module fragment) there might be other translation units in the same module that are able to access things like fn1 without it being exported.
Note also that this does not relate to the private module fragment at all, although an implementation could follow a strategy different from the one accommodated by the standard that did take it into account.
fn3What the private module fragment allows is not function bodies but whole declarations that are not visible in importers: here, of course, that is opaque's definition. We have to define fn3 separately not because we need its definition to be any more hidden than it is by not being inline but because we need to refer to the complete opaque type in that definition. There is still an ABI effect, though: fn3 can't be inline, because the definition of an inline function can't be delayed into the private module fragment, and so the definition of opaque is protected from importers (unless something like LTO is in use).
Again exportation is less relevant than one might imagine. Even though opaque isn't exported, if its definition appeared outside the private module fragment (and we restored the exports) it would be possible for an importer to use it because it is reachable:
import Example;
auto *p = fn3();
auto copy = *p;
struct derived : decltype(copy) {};