Thanks for the feedback.
I made some real progress with the theory, I think.
>
I would also argue that this is not a build-system optimization.
I did not fully understand my own thoughts either. I am truly sorry for that, it's just how my brain works.
I would put it this way:
The originally proposed feature, which is maximally stripped down and quite primitive, is IMO not substantial enough to be a full-blown language feature.
The reason is, it gives us (relatively) few benefits while generating a lot of friction (with existing language rules).
I know that the goal is to discover a new language feature that would allow us to physically hide the implementation details of classes, and is more than a build-system optimization.
What I really meant to say is that I don't think we're there yet.
I already described an "equivalence principle" in my previous emails, which I dub the "inline function equivalence" (pretty sure I am not the first to come up with it though).
My conclusion was that PEMs must be invoked with a special syntax, and must not be found by "regular" name lookup / overload resolution, or else we would end up with an "unsafe" language feature.
I did not know how valuable this insight is, until I found a precedent:
https://en.cppreference.com/cpp/language/access#In_detail> Member access does not affect visibility: names of private and privately-inherited members are visible and considered by overload resolution, implicit conversions to inaccessible base classes are still considered, etc.
> Member access check is the last step after any given language construct is interpreted.
> The intent of this rule is that replacing any `private` with `public` never alters the behavior of the program."
This is very much an equivalence principle to me.
(Silly, I should have found this a while ago. I didn't do my homework, it seems.)
Therefore I think that any rendition of this proposal that fails to address the "inline function equivalence principle" could very well be vetoed by the standard committee.
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.
Encapsulation is not broken.
Interface extensions naturally follow the good ol' class syntax.
They can be naturally placed into an unnamed namespace to get internal linkage if we want that, but it's not mandatory.
But we could also have public or protected interface extensions too:
/// SomeOtherFile.cpp
void UseMyClass(std::vector<MyClass>& xs) {
// Function-local interface extension, why not?
public extension MyClass::ext {
void Gaz() { Bar(0); } // OK, MyClass::Bar(int) is public
// int GetSecret() const { return mySecret; } ERROR: public interface extension cannot access private member MyClass::mySecret
};
std::ranges::for_each(xs, MyClass::ext::Gaz); // OK, MyClass::ext is a public interface extension, usable outside the class too
}
Thoughts?
Sincerely,
Matthew