C++ Logo

std-proposals

Advanced search

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

From: Máté Ték <eppenpontaz_at_[hidden]>
Date: Mon, 22 Jun 2026 15:50:11 +0200
> Have you considered inline facet declarations? As in inline namespaces.

Oh yes, I played around with the idea quite a bit, but in a slightly
different manner.
I thought about "using facet" directives, e.g.
void Use(C& c) {
    using facet C::api;
    c.Foo(); // calls C::api::Foo()
}
I think this would lead to similar ODR issues / unsafe code (move the
function body to a different place that sees the "true" class interface,
and its behavior changes) that made me put the members in a different name
space than the true class members.

I came to think of these facets as faces of a die (cube); only one face
should be on top at any time (you can't roll a 4 and a 6 at the same time).
So a much safer option would be something like a "facet switch", where we
promote the members from a facet, but demote the original class members at
the same time, e.g.:
std::vector<int> v = { ... };
auto& vh = facet_cast<std::vector<int>::heap&>(v);
vh.make_heap(); // promoted from facet
auto beg = vh.::begin(); // demoted original member

I haven't decided yet if I like this or not.
It's somewhat of an uncanny valley or gray area; should you be able to move
the vector through vh?

Dragan Grbic via Std-Proposals <std-proposals_at_[hidden]> ezt írta
(időpont: 2026. jún. 22., Hét 10:30):

> 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
>>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2026-06-22 13:50:30