I used to carefully design my C++ code with public / private / protected / friend (and the pointer-to-impl pattern for "compile time private") but a few years ago I started doing everything all-public ala python (struct instead of class by default) with occasionally an m_ prefix for "intended to be private", and I've never looked back. It's been great, made my programming life much easier.<p>This badge thing is clever, but seems like yet more accidental/unnecessary complexity on top of the already-probably-unnecessary private/protected/friend - as requirements change you now have this extra layer of access-control cruft to reason about, that you probably wouldn't even miss if it didn't exist.<p>Not to get too ranty, and no disrespect to Andreas, but the C++ culture seems to thrive in unnecessary / accidental complexity, deep template hacks and the like that add friction to refactoring, obfuscate underlying logic so you miss opportunities to simplify, and turn straightforward programming problems into brain-bending template puzzles.<p>OOP also; I've seen so many crazy class architectures that could just be a handful of plain-old-functions, complex virutal "type families" that could just be a TypeOfThingThisIs enum with some if statements in the respective functions, etc.<p>I don't know the Serenity codebase and use-case very well, and perhaps this kind of thing has a place in large libraries, but honestly even then I'd lean towards just making everything simple & public and use a prefix when needed, or describe appropriate usage in documentation. Seems to work in the python world (minus a few outlier codebases that go OOP-crazy).
Hello friends! Author here, nice to see so much discussion :)<p>I've spent most of my adult life working on large C++ codebases with public API's (most notably Qt and WebKit), which has led me to cultivate a defensive programming mindset. Someone mentioned Hyrum's law, which is 100% accurate in my experience.<p>I should be honest and admit that Badge<T> is not yet diligently applied everywhere in the Serenity codebase. I wanted to "feel it out" for a while first, but now I've decided I like it, so I'm applying it more and more going forward.<p>Oh and while I have your attention, I recently posted a May 2019 update video[1] on the Serenity OS project if you're curious how things are going :)<p>[1] <a href="https://www.youtube.com/watch?v=KHpGvwBTRxM" rel="nofollow">https://www.youtube.com/watch?v=KHpGvwBTRxM</a>
Personally I'd move the register_device() and unregister_device() functions outside of the VFS class entirely, perhaps to namespace scope.<p>Alternatively, there are other ways to leverage friendship and access control in C++. Here's one option:<p><pre><code> template <typename Registry>
class DeviceManager;
class Device {
template<typename Registry> friend class DeviceManager;
int y;
};
class VFS {
friend struct DeviceManager<VFS>;
int x;
};
template <typename Registry>
struct DeviceManager {
static void register_device (Registry& registry, Device& device) {
registry.x = 1; // accessing privates
device.y = 2; // accessing privates
}
};
int main() {
VFS vfs;
Device dev;
DeviceManager<VFS>::register_device (vfs, dev);
}
</code></pre>
Here all DeviceManager<>'s can reach inside Devices, but only the VFS device manager can reach inside VFS.<p>This is also flexible. Remove 'static' and you get yourself a stateful mediator/manager. Add a variadic template register_devices_s_ member to Devicemanager and you've got yourself a convenience function for registering multiple devices (of heterogeneous types) without bloating the VFS API and, without runtime cost, and without losing type safety (by e.g. casting down to Device& from USBDevice&).
Could you make the badge the last argument and give it a default value, so you don't need the initialization at the function call? It looks a bit strange and would need an explanation for why the empty initialization list is there IMO.<p>I'm not sure if it would work, cppreference has this to say about default arguments:<p>> The names used in the default arguments are looked up, checked for accessibility, and bound at the point of declaration, but are executed at the point of the function call<p><a href="https://en.cppreference.com/w/cpp/language/default_arguments" rel="nofollow">https://en.cppreference.com/w/cpp/language/default_arguments</a>
I do C++ for almost 20 years but never came across this simple technique
before, so I'm quite surprised that I actually like it.<p>I usually avoid 'friend' in these situations and just write<p><pre><code> //private:
</code></pre>
before the method declaration. But it actually happened that co-workers
used these private methods because they use code-completion instead of
reading the header...<p>But I might would call the class Friend<T> or something.
I like it but is access restriction really a problem? In my years of C++ programming it always seemed to be a theoretical problem more than something that leads to crashes.
A different pattern I have thought of would be to provide an accessor interface object, and certain functionality is limited to be accessible only through that object. Controlling who has access to that interface can be flexible (possibly returning it along with construction, or passing it in at creation time...) Albeit, this is not without overhead, and for that, Badge is actually better, in terms of performance, if it fits the use case.
What happens when you have multiple Device objects though, in this case, they can all access VFS..<p>--<p>C++'s private inheritance can be used here to avoid some of the gymnastics, wherein, that private aspect of the class can be passed out into the appropriate party at creation time or otherwise.<p>To illustrate: <a href="https://gist.github.com/arithma/0d96aec5c07e2d8dc24cfc6cd31f0c16" rel="nofollow">https://gist.github.com/arithma/0d96aec5c07e2d8dc24cfc6cd31f...</a><p>Note: I have never used this in production as far as I remember.
I like it! The self-documentation of where the call is coming-from is quite nice (though a more descriptive name like CallFrom<T> would make that even clearer, were it the principle intent). I can't help myself, though:<p><pre><code> template<typename T>
Badge<T> FakeBadge() {
struct Stub {}
return reinterpret_cast<Badge<T>>(Stub());
}</code></pre>
This reminds me of the Passkey idiom: <a href="https://arne-mertz.de/2016/10/passkey-idiom/" rel="nofollow">https://arne-mertz.de/2016/10/passkey-idiom/</a>
How is this different from the (relatively common afaik) passkey idiom <a href="https://arne-mertz.de/2016/10/passkey-idiom/" rel="nofollow">https://arne-mertz.de/2016/10/passkey-idiom/</a> ?
This is a typical example of a solution to an artificial, self-imposed problem created by C++'s assumptions.<p>As a programmer, I would feel frustrated to have to figure this out instead of spending time on something a user would actually care about.
This is especially helpful in conjunction with private constructors and make_shared or make_unique. You cannot even friend make_shared because of namespace aliasing in standard libraries, but shared_from_this can make improperly allocated objects error at runtime. Private badges/tokens solve that problem by allowing public constructors requiring a parameter value of the badge/token type that has limited visibility, but can be constructed inside a factory and passed to make_shared.
I often wonder how this would map to other languages (as C++ often entails complexity and clever work arounds such as this article).<p>In Java or C# (and of course in C++), you could have a token system where the token can only be provided by Device :<p>VFS:
void register(DeviceToken):<p>DeviceToken: empty interface (marker)<p>Device:
private DeviceToken provideDeviceToken();<p>This hides both VFS and Device classes from accessing each others' private members.<p>You can also constrain how gets to provide DeviceToken by adding sufficient comments in code.
C++ could solve this without badges, if it didn't have a very tiny, silly misfeature.<p>The misfeature is this: a class A can only declare a specific member function of class B as a <i>friend</i>, if that B member function is public!<p>Example:<p>The idea here is that Device has a static member function called <i>Device::registrationHelper</i>. That specific function (not the entire Device class) is declared a friend to VFS, and so that function can call <i>VFS::registerDevice</i>.<p>[Edit: this doesn't quite match the Badge solution, though. If this worked, it would have the problem that the <i>registrationHelper</i> has access to all of <i>VFS</i>, which is almost as bad as the entire <i>Device</i> class being a friend of <i>VFS</i>. That is one of the problems which motivate Badge. Nice try, though! The C++ restriction is probably worth fixing anyway. What the C++ friendship mechanism really needs here is to be able to say that a specific member function in B is allowed to access a specific member of A.]<p><pre><code> class Device;
class VFS;
class Device {
public: // we want this private!
static void registrationHelper(VFS &v, Device &d);
private:
void registerWith(VFS &vfs) { registrationHelper(vfs, *this); }
};
class VFS {
private:
void registerDevice(const Device &);
friend void Device::registrationHelper(VFS &, Device &);
};
void Device::registrationHelper(VFS &v, Device &d)
{
v.registerDevice(d);
}
</code></pre>
But we are forced to make <i>Device::registrationHelper</i> public, which defeats the purpose: anyone can call it and use it is a utility to register devices with VFSs.<p>If we make Device::registrationHelper private to prevent this, then the "friend void Device::registrationHelper" declaration in VFS fails.<p>This is an oversight; the privacy of the <i>Device::registrationHelper</i> identifier means that the <i>VFS</i> class is not even allowed to mention its name for the sake of declaring it a friend.<p>That should be fixed in C++. Declaring a function a <i>friend</i> shouldn't require it to be accessible; we are not lifting its address or calling it; we are just saying that it can call us. Allowing a function to call us is not a form of <i>access</i> to that function, yet is being prevented by <i>access</i> specifiers.
I might be wrong, but doesn't it look like a way to enforce SRP violation? In the provided example we don't only let the device know how to register itself via VFS, but we also make sure there will be no future DeviceManager to do that. Unless we duplicate the entire interface with Badge<DeviceManager> tag. Am I missing any non-obvious benefits of this approach?
In D, you can access private members of a class within a single module. I like it, because it allows to move methods outside of the class, but doesn't require explicit friend declarations. And you still have the data protection because other modules can't access the private fields.
I had never seen this before, I cannot think of another way to do this. I suspect it has 1 byte overhead (on the stack) cost, but that's pretty cheap and may be optimized out.
This is essentially a capability system enforced by the compiler, which means your code is not actually in control for any third party caller.<p>A glaring security hole. Any old hacker can forge or clone a data structure. This "badge" (AKA token) has to be explicitly unpredictably replay-proof generated and hard to forge, and also automatically verified.
Seems more of a people problem than a software problem. That's normal. Why not just use a comment that says "Don't use this. It will break."? If people can't obey simple instructions, you have a different problem entirely.
whats the advantage over declaring register_device as a functor object from an anon class?
for example:<p><pre><code> class Device;
class VFS {
public:
class {
friend Device;
void operator()(int y) {/* do stuff with y here*/}} register_device;
};
class Device {
public:
void foo(VFS fs) {
fs.register_device(1);
}
};
int main(int argc, char *argv[]) {
// fails:
// VFS().register_device(2);
// works:
Device().foo(VFS());
return 0;
}
</code></pre>
and no need for a badge class (erm... well but an anon class).<p>my c++ skills are a bit rusty, because most of the time python works just fine.
Wouldn't -Wextra yell about the Badge being an unused parameter in the register devices? You would have to do some kind of dummy call that the compiler would have to optimize away.
> These functions are called by all Device objects when they are constructed and destroyed respectively. They allow the VFS to keep track of the available device objects so they can be opened through files in the /dev directory.<p>> Now, nobody except Device should ever be calling VFS::register_device() or VFS::unregister_device(),<p>Why?<p>Either have VFS call the constructor because non-registered devices are banned and RAII is good, or don't create arbitrary restrictions to hamstring how the rest of the system manages Devices.