Contents
After last week’s post about tag dispatch let’s have a look at another example for useful empty classes: The passkey idiom can help us regain control that we would give up by simply making classes friend
s.
The problem with friendship
Friendship is the strongest coupling we can express in C++, even stronger than inheritance. So we’d better be careful and avoid it if possible. But sometimes we just don’t get around giving a class more access than another.
A common example is a class that has to be created by a factory. That factory needs access to the class’ constructors. Other classes should not have that access to not circumvent the bookkeeping or whatever else makes the factory necessary.
A problem of the friend
keyword is that it gives access to everything. There is no way to tell the compiler that the factory should not have access to any other private elements except the constructor. It’s all or nothing.
class Secret {
friend class SecretFactory;
private:
//Factory needs access:
explicit Secret(std::string str) : data(std::move(str)) {}
//Factory should not have access but has:
void addData(std::string const& moreData);
private:
//Factory DEFINITELY should not have access but has:
std::string data;
};
Whenever we make a class a friend
, we give it unrestricted access. We even relinquish the control of our class’ invariants, because the friend
can now mess with our internals as it pleases.
The passkey idiom
Except there is a way to restrict that access. As so often, another indirection can solve the problem. Instead of directly giving the factory access to everything, we can give it access to a specified set of methods, provided it can create a little key token.
class Secret {
class ConstructorKey {
friend class SecretFactory;
private:
ConstructorKey() {};
ConstructorKey(ConstructorKey const&) = default;
};
public:
//Whoever can provide a key has access:
explicit Secret(std::string str, ConstructorKey) : data(std::move(str)) {}
private:
//these stay private, since Secret itself has no friends any more
void addData(std::string const& moreData);
std::string data;
};
class SecretFactory {
public:
Secret getSecret(std::string str) {
return Secret{std::move(str), {}}; //OK, SecretFactory can access
}
// void modify(Secret& secret, std::string const& additionalData) {
// secret.addData(additionalData); //ERROR: void Secret::addData(const string&) is private
// }
};
int main() {
Secret s{"foo?", {}}; //ERROR: Secret::ConstructorKey::ConstructorKey() is private
SecretFactory sf;
Secret s = sf.getSecret("moo!"); //OK
}
A few notes
There are variants to this idiom: The key class need not be a private member of Secret
here. It can well be a public member or a free class on its own. That way the same key class could be used as key for multiple classes.
A thing to keep in mind is to make both constructors of the key class private, even if the key class is a private member of Secret
. The default constructor needs to be private and actually defined, i.e. not defaulted, because sadly even though the key class itself and the defaulted constructor is not accessible, it can be created via uniform initialization if it has no data members.
//...
ConstructorKey() = default;
//...
Secret s("foo?", {}); //Secret::ConstructorKey is not mentioned, so we don't access a private name or what?
There was a small discussion about that in the “cpplang” Slack channel a while ago. The reason is that uniform initialization, in this case, will call aggregate initialization which does not care about the defaulted constructor, as long as the type has no data members. It seems to be a loophole in the standard causing this unexpected behavior.
The copy constructor needs to be private especially if the class is not a private member of Secret
. Otherwise, this little hack could give us access too easily:
ConstructorKey* pk = nullptr;
Secret s("bar!", *pk);
While dereferencing an uninitialized or null pointer is undefined behavior, it will work in all major compilers, maybe triggering a few warnings. Making the copy constructor private closes that hole, so it is syntactically impossible to create a ConstructorKey
object.
Conclusion
While it probably is not needed too often, small tricks like this one can help us to make our programs more robust against mistakes.
Permalink
Shenanigans with the broken
ConstructorKey() = default;
are now fixed in C++20.https://godbolt.org/z/fc5Mjas7K
Permalink
Permalink
Wouldn’t this work just as well?
http://ideone.com/4vUgbK
The idea is to delegate the construction to a intermediate factory class that we sure will not abuse its friends rights.
Does not rely on obscure construction interaction, does not pollute class constructor signature, should be completely portable.
Permalink
although I never used the idiom (on purpose at least), I think the idea is that the idiom works with any member function, not just constructors.
Permalink
What would you say are the advantages and disadvantages of passkey over the Attorney-Client idiom:
http://www.drdobbs.com/friendship-and-the-attorney-client-idiom/184402053
Permalink
Nice article. There are also other use cases for this kind of idiom, e.g. http://bit.ly/2e3nr95
Permalink
I don’t think your example is not correct; ConstructorKey as written is an aggregate, and it seems like {} initialization directly goes into aggregate initialization, bypassing any access checks. Live example: http://coliru.stacked-crooked.com/a/243d61fc72b38e4b. Changing the defaulted constructor to an empty one counts as a “user-defined constructor”, making ConstructorKey a non-aggregate, making aggregate initialization impossible, making it work.
Permalink
Thanks for pointing this out. In fact, it was this case of empty-aggregate initialization ignoring the private default constructor that was somewhat surprising. I updated the key class and the section in question.