In contrast, properties of a non-exported entity are reachable from the outside according to the "attendant entities" rules in 10.7.6. A special error condition in paragraph 3 ensures a consistent view from the outside. Example:
From the outside (i.e. from a module or main program importing M), decltype(f()) would access an incomplete S and decltype(g()) would access the complete S, which would be inconsistent. The resolution in the Modules TS is to make a module giving rise to this situation ill-formed.export module M; struct S; export S f(); // #1; cannot call this function (incomplete return type) struct S { }; export S g(); // error: attendant class type S has different properties vs. #1 (see 10.7.6p3)
However, there are other situations where different access paths are resolved by essentially simply exporting the complete type. Example:
By design, nested types are not attendant entitites and thus the completeness ofexport module M; struct C { }; struct S { struct B { }; using C = ::C; }; export S f(); // #1; S::B and ::C are not attendant entities export C g(); // #2; ::C is an attendant entity // elsewhere import M; int x = sizeof(decltype(f())::B); // error: incomplete class type B int y = sizeof(decltype(f())::C); // ok: C is complete
S::B
or S::C
is not
reachable from the outside given only #1. However, the completeness
can be added to the reachable properties by a later, unrelated
declaration such as #2. In this case, the access path through #1 will
see the complete type S::C
.
It seems strange to treat the two examples differently. For the first
example, instead of making the program ill-formed, an alternative
resolution that preserves the consistency design goal is to simply
consider S
a complete type from the outside. The uniform
rule would be "the properties of an entity reachable from the outside
are the union of those made available through contributing
declarations". This rule also addresses exported entities, where (of
course) only export-declarations contribute.
It is recommended to treat this as a Defect Report against the Modules TS.
export
and later redeclaring (or defining)
without export
:
This allows programmer control of the properties visible from the outside.export module M; export int f(int); int f(int = 5); export struct S; struct S { int x; }; export constexpr int g(int); constexpr int g(int x) { return x+1; } // elsewhere import M; int x = f(); // error: default argument not visible S y; // error: S is not complete constexpr int z = g(42); // ok
However, this does not apply to definitions of inline functions. If an inline function is exported, its definition must always appear in the module interface unit (10.1.2 [dcl.fct.spec]) and the definition itself is implicitly exported (10.7.6p1 [dcl.module.reach]). (It is understood that a definition of an inline function must be reachable whenever it is called.)
Assume thatexport module M; export class S { public: // public, exported interface protected: void f(); // helper function, exported "by accident" }; export class D : public S { public: void g(); };
S::f
should get an inline definition, but will
only be called from a single module implementation unit (the one
implementing D::g
). It seems the only currently valid
place where to put the inline definition is the module interface unit,
also implicitly making that inline definition exported. However, to
implement the use-case, it would be sufficient (and expose fewer
things) to put the inline definition into the module implementation unit
for D::g
.
Further, a module importing M could inspect S::f
for its
type (return type, parameter types), and such inspection would not
need the definition of S::f
.
The reasoning that supports selectively exporting default function arguments or class type definitions should also apply here: Give the programmer control of what is and is not exported.
Since constexpr functions are implicitly inline, a change here does also apply to constexpr functions.
It is recommended to treat this as a Defect Report against the Modules TS.
export
can be used to export a single
declaration, or a brace-enclosed sequence of declarations. However,
the syntactic treatment is different from linkage specifications
[dcl.link], a similar context where such a construction is allowed.
A linkage specification is ignored for all declarations to which it does not apply. In contrast, attempting to export a declaration that cannot be exported (e.g. aextern "C++" { int f(); static_assert(1 == 1, "blah"); // ok static int g(); // ok; linkage specification does not apply } export { int f(); static_assert(1 == 1, "blah"); // error: does not declare a name static int g(); // error: cannot export function declared static }
static_assert
) makes the
program ill-formed.
Assuming that both constructs are intended to change a given property
of a possibly large set of pre-existing declarations with minimal
syntax, I suggest to allow such vacuous exports. Pre-existing sets of
declarations that are converted to modules might reasonably
contain static_assert
s or unnamed namespaces in the
middle of to-be-exported declarations. Rearranging the code might be
too much of a burden.
It is recommended to treat this as a Defect Report against the Modules TS.
There is no way to nameexport module M; struct S { }; class C { public: void f(); private: S g(); // helper function }; export C h(); // S is among the attendant entities
C::g()
from outside the module
defining C
, thus there is no way to name the return type
of C::g()
. Consistent with the approach to clearly
delineate what is visible from the outside, I suggest to remove
consideration of private members for attendant entities.
It is recommended to treat this as a Defect Report against the Modules TS.
If X is an attendant entity of two exported declarations designating two distinct entities, its reachable semantic propertiesEditorial note: Maybe this rule can be folded into paragraph 2.shall be the same at the points where the declarations occurare the union of those introduced where the declarations occur. [ Example:export module M; struct S; export S f(); //-- end example ]#1struct S { }; export S g(); //error: class type S has different properties from #1ok // translation unit 2 import M; decltype(f()) x; // ok, entity S is a complete type
Drop the change to 10.1.2 [dcl.fct.spec]:
Remove in 10.7.6 [dcl.module.reach] paragraph 1 bullet 1:An exported inline function shall be defined in the same translation unit containing its export declaration. [ Note: There is no restriction on the linkage (or absence thereof) of entities that the function body of an exported inline function can reference. A constexpr function (10.1.5) is implicitly inline. -- end note ]
Change in 10.7.6 [dcl.module.reach] paragraph 2, second list, bullet 3:
- ...
Furthermore, if D denotes an inline function, the property that the inline function has a definition (10.1.2) is a reachable semantic property, even if that definition is not exported.Otherwise,
- ...
- If T is a class type owned by M, the set of attendant entities includes T itself, the union of the sets of the attendant entities determined by its direct public or protected base classes owned by M, the sets of the attendant entities of its data members, static data member templates, member functions, member function templates, the function parameters of its constructors and constructor templates, as far as these members are public or protected. Furthermore, if T is a class template specialization, the set of attendant entities also includes: the class template if it is owned by M , the union of the sets of attendant entities determined by the type template-arguments, the sets of the attendant entities of the templates used as template template-arguments, the sets of the attendant entities determined by the types of the non-type template-arguments.
- ...