C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Translation-unit-local functions that access private class fields

From: Dragan Grbic <dgrbic_at_[hidden]>
Date: Mon, 22 Jun 2026 10:30:12 +0200
Have you considered inline facet declarations? As in inline namespaces.
It would allow API declaration in the facet, and then on the call site to
access the api-declared members directly.

//////// C_api.hpp ////////
class C;
inline facet C::api { // Notice, no access specifier is needed here
    void Foo(int); // Cannot be defined inline, would be a compilation error
};
//////// C.hpp ////////
#include "C_api.hpp"
class C {
    int m_secret = 0;
    inline facet api; // Authorizes the facet - must be declared inline, as
in C_api.hpp
};
//////// C.cpp ////////
#include "C.hpp"
void C::api::Foo(int i) { m_secret += i; } // OK, class is seen,
authorization OK

//////// SomeSourceFile.cpp ////////
#include "C_api.hpp"
void Use(C& c) { c.Foo(5); } // As api facet is declared inline, Foo is
directly accessible


On Mon, Jun 22, 2026 at 1:47 AM Máté Ték via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Hi!
>
> I also had time to think, and I'd like to share what I found.
> I'll give you my train of thought I had some days ago, some of which I
> will backtrack, because I discovered new theory since.
> Please, do not take this too seriously.
> I'm not trying to say "my approach is good, your approach is bad", nothing
> like that.
> I just want to say "I explored in this direction, and look what I found".
>
> > I've been thinking about the feature here and I think that re-opening
> the class scope is out-of-scope for this problem.
> I don't think we can avoid "re-opening the class scope".
> It's exactly what we want to do! Even if we only add new *private* members
> for a start.
>
> I suppose that "re-opening the class scope" means adding a new entity to
> the "namespace of the class", i.e.
> fully_qualified_class_name::new_member
> Mathematically speaking (and syntactically in C++), we can think of a
> class as a kind of namespace, with special rules.
> Until now, it was forbidden to add a new entity to this namespace outside
> the class definition.
> But a PEM is exactly that; a late addition to this namespace, i.e. we just
> "re-opened" the "class scope".
> So really we are asking for trouble here, and we should not be afraid to
> admit it.
>
> Compare this with "regular" namespaces:
> - We're allowed to add new entities to them freely at any time (more or
> less).
> - Not all TUs or all contexts need to see *all* entities that exist in a
> namespace in the whole C++ program ("partial discovery").
> Why not allow the same for class members?
>
> So I embraced this concept, and I wanted to see how far I can take
> it, without breaking the language of course.
> We should be able to add new members without the permission of the class.
> (**I'm going to backtrack this**)
> We already know a couple caveats:
> - Only non-ABI-relevant members can be added (no virtual functions, no
> non-static data members, etc.)
> - Semantics of the class must be preserved, i.e. no special members like
> copy ctor, etc.
> - Encapsulation safety must be preserved.
> - Adding members directly to the "namespace" fully_qualified_class_name:: is
> problematic and can lead to ODR issues or unexpected bugs.
>
> The last point can be circumvented if the new members *must* live in a
> dedicated "in-class namespace", e.g.
> fully_qualified_class_name::ext::member
> I call these new in-class namespaces "facets". It's the best term I could
> come up with instead of "extensions".
>
> I don't think "extension" is the right term. I don't think it captures the
> essence of what we are trying to do.
> We are trying to split the interface of a class, on purpose, into
> different logical partitions, e.g. public/ABI interface vs implementation.
> Hiding some of it should not be the sole fruitage of this new language
> feature, more like a "deliberate byproduct".
> It should enable us to do so, but should not be limited to just that.
> Also, "extension" implies (to me) that the thing we are extending is
> already complete in some sense, i.e. without the extension, the remainder
> should still be a valid, self-contained entity, just with less capabilities.
> However, if we take away the "private implementation extension" of the
> class, then it ceases to function, because it was an integral part of the
> class.
> Hence I looked for a different term.
>
> I identified a couple key thoughts to preserve encapsulation safety:
> - We can introduce new members that have unconditional private access to
> a class, but only if they themselves are only usable in contexts that
> already have private access to the class.
> - Such entities with private access *must not* be allowed to declare
> friends, only the base class should be able to grant friend access to third
> parties.
> - Similarly, if we want to introduce new members that are usable by
> anyone (public extension), then they must only be able to operate with the
> public interface of the class, unless they are granted friend access by the
> class itself.
>
> From these, I derived the following rudimentary rules for these "facets":
>
> 1. One facet is associated with exactly one class. A class can have
> multiple associated facets.
> 2. The base class does not need to know about all associated facets.
> External facets can be attached without modifying the class definition.
> 3. Facets must have a unique name (cannot be unnamed) that is
> different from the name of the class and all parent classes.
> 4. The facet itself acts as a sort of "namespace" within the class,
> the entities (e.g. member functions) of the facet shall appear to be in the
> "namespace"
> fully_qualified_class_name::facet_name
> 5. No "ADL for facet members"; to use them, they must be referred to
> with their fully qualified name, except the class name part can be omitted
> if it is unambiguous, e.g. this->Foo() always invokes the
> "non-facetal" member function Foo() whereas the "facetal" member
> function is invoked with this->facet::Foo().
> (It's not mandatory to use the explicit this-> notation, I just used
> it for clarity.)
> 6. Facets must be assigned a single access specifier (public,
> protected, private), that controls both the access to the class members
> within the facet, and the accessibility of the facet members themselves,
> e.g. the members of a private facet shall behave as if they were private
> members of the class, with private access to the class.
> 7. Facets can only contain entities that would make sense as members
> of a class, but do not affect class layout, i.e. no virtual functions, no
> non-static data members, and do not affect class semantics, i.e. no special
> member functions like copy ctor, etc.
> 8. Also, no friend declarations inside facets. Only the base class
> should be able to grant friend access to a third party.
> 9. Facets can be predeclared and befriended, possibly by the base
> class itself.
> 10. Facets (more precisely, its members) can have different linkage
> than their base class.
> 11. Facets can be defined in-class, in which case they have private
> access to the base class, regardless of their own access specifier.
>
> Here are some possible use cases that we get from these rules:
> Example 1: Hiding private implementation details (the original proposal)
> //////// C.hpp ////////
> class C {
> int m_secret;
> public:
> void Foo();
> };
> //////// C.cpp ////////
> #include "C.hpp"
> namespace {
> private facet C::impl {
> void Foo() { ++m_secret; }
> };
> }
> void C::Foo() { impl::Foo(); }
>
> Or we could hide the entire class layout (!!) with a forward-defined
> public facet, which IMO is even more amazing.
> Example 2
> //////// C_api.hpp ////////
> class C;
> public facet C::api {
> void Foo(int); // Defined in C.cpp where it has private access
> };
> //////// SomeSourceFile.cpp ////////
> #include "C_api.hpp"
> void Use(C& c) { c.api::Foo(5); }
> //////// C.hpp ////////
> #include "C_api.hpp"
> class C {
> int m_secret = 0;
> friend C::api; // Syntax debatable. "friend facet api;"?
> };
> //////// C.cpp ////////
> #include "C.hpp"
> void C::api::Foo(int i) { m_secret += i; }
>
> Example 3: Restrict the scope of friend access to a facet.
> It's always a good idea to reduce the scope of friend access. Facets would
> allow more granularity.
> //////// C.hpp ////////
> class D;
> private facet D::authorized;
> class C {
> int m_secret;
> friend D::authorized;
> };
> //////// D.hpp ////////
> #include "C.hpp"
> class D {
> facet authorized {
> static int GetSecret(const C& c) { return c.m_secret; }
> }
> };
>
> Example 4: Using facets to refactor an unwieldy overgrown class.
> We've all been there.
> This may be an intermediate step when untangling the class, but definitely
> better than leaving it unorganised.
> class Unwieldy {
> facet event {...} // Members related to event handling
> facet thread {...} // Members related to threading
> ...
> };
>
> Example 5A: Defining entirely new operations for existing classes, using
> only their public API
> template<class T, class A>
> public facet std::vector<T, A>::ptr {
> // Public facet -> can only use public interface of vector
> template<typename Self>
> auto begin(this Self&&) { return data(); }
> template<typename Self>
> auto end(this Self&&) { return ptr::begin() + size(); } // must prefix
> even here
> template<typename Self>
> auto at(this Self&&, size_type pos) { return &(at(pos)); } // 'at()' =
> true member
> };
> // Usage:
> std::vector<int> v = { /* ... */ };
> int* fifth = v.ptr::at(5);
>
> Example 5B
> template<class T, class A>
> public facet std::vector<T, A>::heap requires(...) { // Comparable T
> void make_heap() { std::make_heap(begin(), end()); }
> void push(const T& x) { push_back(x); std::push_heap(begin(), end()); }
> void pop() { /* ... */ }
> const_reference top() const { /* ... */ }
> };
> // Usage:
> std::vector<int> v = { /* ... */ };
> v.heap::make_heap();
> int top = v.heap::top();
> v.heap::pop();
>
>
> Here comes the backtracking.
> As much as I'd love to be able to do something like examples 5A/5B,
> there's an issue here:
> What if I used a static library in my code, which also defined its own
> vector heap facet, and I have one too?
> This would be an amazing "interface glue" feature, but name collision
> becomes an issue again.
> We could try to find a solution to this, e.g. the new members *do not*
> live in the "namespace of the class", but somewhere else.
> E.g. we could have
> namespace myOrg {
> public facet vector<...>::heap { ... };
> }
> and the new "members" would appear as having class member type but in the
> myOrg namespace:
> namespace myOrg {
> void (std::vector<T, A>::* make_heap)();
> }
> But this is a whole new level of scary, and I did not go further.
>
> The second backtrack is:
> I made an arbitrary choice, and said "we should be able to add new members
> without the permission of the class".
> I later realized, this is a very important aspect.
> > Can public interface extensions only access public members (with an
> implicit this pointer)
> > or are they themselves publicly accessible?
> > From your post both seems to be true. And it is mixing two concepts.
> If we don't need the permission of the class to create new facets, then a
> public extension (facet) must only have public access to the class,
> otherwise we break encapsulation.
> So yes, I am mixing the two concepts, because they are not independent,
> and it's the only way to not break the language.
> However, if we require the class' permission to define facets anyhow, then
> all facets can have private access to the class, which is a dramatic
> change, and we are no longer mixing the two concepts.
> So something like
> class C { facet impl; ... }; // Facet *must* be declared by the class
> itself
> It is still possible to "forward define" an API for a class and hide its
> memory layout, like in Example 2, if the rule is the following:
> A facet interface can be declared without seeing the class definition,
> however, at the point of definition of the facet members, the class must be
> seen and the facet must be "authorized" by the class.
> So something like this should work:
> //////// C_api.hpp ////////
> class C;
> facet C::api { // Notice, no access specifier is needed here
> void Foo(int); // Cannot be defined inline, would be a compilation
> error
> };
> //////// C.hpp ////////
> #include "C_api.hpp"
> class C {
> int m_secret = 0;
> facet api; // Authorizes the facet
> };
> //////// C.cpp ////////
> #include "C.hpp"
> void C::api::Foo(int i) { m_secret += i; } // OK, class is seen,
> authorization OK
> Due to these, it also becomes possible to then have facet members with
> "mixed access specifiers", e.g.
> facet C::impl {
> public:
> void Foo();
> private:
> void Bar();
> };
> But this comes down to whether we require the base class' permission or
> not!
> This final approach I showed, seems safer and more conservative.
> We can no longer attach new arbitrary facets to an existing class.
> It becomes more of a code organisational tool only for the author of the
> class, which may or may not be a good thing.
>
> > I think the main problem with reopening the class scope is that
> suddenly you might be able to add a new function to library code where it
> was not intended for the user to be able to add library code, especially
> not new code that can access the library's internals.
> Then I suppose you would endorse this last approach.
> Maybe we can take the best parts of all of our ideas, and knead them into
> something even better?
> Thoughts?
> I will try to reply to your latest mails, I see there are many.
>
> Sincerely,
> Matthew
>
> On Fri, 19 Jun 2026 at 23:01, Rhidian De Wit via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> Assuming the functions are inlined I would think?
>> Otherwise we would get ODR violations if I'm not mistaken. Consider the
>> following:
>> // Foo.h
>> class Foo {
>> private:
>> int m_var;
>>
>> public:
>> Foo();
>> };
>>
>> private impl Foo {
>> void Print() {
>> std::cout << m_var;
>> }
>> }
>>
>> // Foo.cpp
>> Foo::Foo() {
>> m_var = 10;
>> Print(); // First Translation Unit with 'Print()' defined. OK
>> }
>>
>> // Bar.cpp
>> Bar::Bar(Foo & fooInstance) {
>> fooInstance.Print(); // 2nd Translation Unit with 'Print()' defined.
>> ODR Violation!
>> }
>>
>> Inlining Foo should fix the ODR violation, but is that something that
>> we'd even want to allow? PEMs are *private* extension methods, if we
>> allow them to be shared across TU's they wouldn't be private anymore and
>> we'd get closer again to the discussion of reopening the class scope.
>> I think the principle of PEMs should be to not be shared at all. If you
>> want to share helper methods, you can always just define a free function or
>> a public (static) member function and not worry about extra ODR, function
>> overloading, ... rules
>>
>> I do think that reopening the class scope holds some merit to it, but I
>> think PEMs are a good starting off point for such a thing:
>> We can test out privately reopening the class scope and can then further
>> extend it to *public* extension methods that *can* be shared among TUs.
>>
>> Best,
>>
>> Rhidian
>>
>> Op vr 19 jun 2026 om 20:50 schreef Sebastian Wittmeier via Std-Proposals <
>> std-proposals_at_[hidden]>:
>>
>>> I assume, if you want to share the PEM between translation units after
>>> all,
>>>
>>> you can just put the PEM definition inside a (header) file and include
>>> it from every translation unit using it.
>>>
>>>
>>> -----Ursprüngliche Nachricht-----
>>> *Von:* Rhidian De Wit via Std-Proposals <std-proposals_at_[hidden]>
>>> *Gesendet:* Fr 19.06.2026 20:44
>>> *Betreff:* Re: [std-proposals] Translation-unit-local functions that
>>> access private class fields
>>> *An:* std-proposals_at_[hidden];
>>> *CC:* Rhidian De Wit <rhidiandewit_at_[hidden]>;
>>> Hi all,
>>>
>>> I've been thinking about the feature here and I think that re-opening
>>> the class scope is out-of-scope for this problem.
>>> I think the main problem with reopening the class scope is that suddenly
>>> you might be able to add a new function to library code where it was not
>>> intended for the user to be able to add library code, especially not new
>>> code that can access the library's internals.
>>>
>>> Therefore, I think the PEMs should remain local to a translation unit to
>>> avoid ODR violations and to avoid breaking encapsulation of external
>>> libraries.
>>>
>>> I do think something akin to Rust's impl blocks would be nice, but I
>>> think that's where PEMs come in. We could define a PEM as:
>>> // Foo.h
>>> class Foo {
>>> private:
>>> int m_var;
>>>
>>> public:
>>> Foo();
>>>
>>> void RecalculateVar();
>>> };
>>>
>>> // Foo.cpp
>>> private impl Foo {
>>> int CalculateNewVar() {
>>> int newVar = // Complex calculation goes here...
>>> return m_var + newVar;
>>> }
>>> }
>>>
>>> Foo::Foo()
>>> : m_var(0)
>>> {
>>> m_var = CalculateNewVar();
>>> }
>>>
>>> void Foo::RecalculateVar() {
>>> // Gets called by some thread every X minutes
>>> m_var = CalculateNewVar();
>>> }
>>>
>>> I think that's a nice start to PEMs. We can move complexity and symbols
>>> out of the header file yet allow functions access to private member
>>> variables and prevent any ODR violations in the process.
>>> I would also then mandate that functions in PEMs cannot overload
>>> non-static member functions because it would greatly improve teachability
>>> rather than explaining why your best fit overload defined in a PEM does not
>>> get selected for overload resolution.
>>>
>>> Best,
>>>
>>> Rhidian
>>>
>>> Op vr 29 mei 2026 om 06:05 schreef Mital Ashok via Std-Proposals <
>>> std-proposals_at_[hidden]>:
>>>
>>> On Thu, 28 May 2026, 16:51 Máté Ték via Std-Proposals, <
>>> std-proposals_at_[hidden]> wrote:
>>>
>>>
>>> The real issue with classic PEMs IMO is that they live in the same "name
>>> space" as the "first-class class members".
>>> This is what generates the friction with overload resolution/ODR.
>>> So I thought, let's give them a "name space" within the class they're
>>> extending!
>>> After some fooling around with various syntaxes, here's my strongest
>>> candidate:
>>>
>>> /// MyClass.hpp
>>> class MyClass {
>>> public:
>>> void Foo(int);
>>> void Bar(int);
>>> private:
>>> int mySecret;
>>> };
>>>
>>> /// MyClass.cpp
>>> namespace {
>>> private extension MyClass::impl {
>>> void FooBar(int i) { mySecret += i; }
>>> // virtual void Gaz(); ERROR: extension member function cannot
>>> be virtual
>>> // int otherSecret; ERROR: extension cannot have non-static
>>> data members
>>> static int otherSecret; // OK, why not?
>>> };
>>> // I could have defined FooBar out-of-line like so:
>>> void MyClass::impl::FooBar(int i) { mySecret += i; }
>>> }
>>> void MyClass::Foo(int i) { impl::FooBar(i + 1); }
>>> void MyClass::Bar(int i) { impl::FooBar(i * 2); }
>>> // static void SomeFunction(MyClass& x) { x.impl::FooBar(42); } ERROR:
>>> class interface extension MyClass::impl is private
>>>
>>> Thus we eliminated the overload resolution/ODR concerns.
>>> It is now very clear that PEMs are not "first-class" members of the
>>> class.
>>> The interface of the class, and therefore its "identity" is undisputed
>>> and unfractured.
>>>
>>>
>>> A similar thing can be implemented with existing language features:
>>>
>>> // header
>>> class X {
>>> private:
>>> // ...
>>> struct impl;
>>> friend impl;
>>> public:
>>> int public_interface();
>>> };
>>>
>>> // source
>>> struct X::impl {
>>> static int helper(X& self) {
>>> // ...
>>> }
>>> };
>>> int X::public_interface() {
>>> return impl::helper(*this);
>>> }
>>>
>>>
>>> This loses the easier syntax of an implicit object argument and has
>>> external linkage (though you can work around that with `namespace detail {
>>> namespace { struct X_helper; } } ` and `friend detail::X_helper;`), but is
>>> implementable today without exposing too much in the header.
>>>
>>>
>>> Encapsulation is not broken.
>>> Interface extensions naturally follow the good ol' class syntax.
>>>
>>>
>>> Encapsulation/access control does seem broken. This could allow private
>>> member access in any translation unit by just defining an extension. This
>>> could be fixed by requiring a declaration of the extension but that is
>>> closer to something like Java interfaces as opposed to Rust impl/traits
>>> --
>>> Std-Proposals mailing list
>>> Std-Proposals_at_[hidden]
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>
>>>
>>>
>>> --
>>> Rhidian De Wit
>>> Software Engineer - Barco
>>>
>>> --
>>> Std-Proposals mailing list
>>> Std-Proposals_at_[hidden]
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>
>>> --
>>> Std-Proposals mailing list
>>> Std-Proposals_at_[hidden]
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>
>>
>>
>> --
>> Rhidian De Wit
>> Software Engineer - Barco
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2026-06-22 08:30:37