Passkey Idiom: A Useful Empty Class

Passkey Idiom: A Useful Empty Class

By Arne Mertz

Overload, 31(176):18-19, August 2023


How do you share some but not all of a class? Arne Mertz introduces the passkey idiom to avoid exposing too much with friendship.

Let’s have a look at an example for useful empty classes. The passkey idiom can help us regain the control that we give up by simply making classes friends.

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 can’t get around giving one class more access than another.

A common example is a class that has to be created by a factory. The factory needs access to the class’s constructors. Other classes should not have that access so as not to circumvent the bookkeeping or whatever else makes the factory necessary.

The problem with 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. See Listing 1.

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;
};
Listing 1

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

There is a way to restrict that access. As so often is the case, 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. See Listing 2.

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
}
Listing 2

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 are not accessible, it can be created via uniform initialization [Mertz15] 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 [Slack] 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 behaviour.

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 behaviour, 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 is probably not needed often, small tricks like this one can help us to make our programs more robust against mistakes.

References

[Mertz15] Arne Mertz ‘Modern C++ Features – Uniform Initialization and initializer_list’, posted 5 July 2015 at: https://arne-mertz.de/2015/07/new-c-features-uniform-initialization-and-initializer_list/

[Slack] Cpplang discussion: https://cpplang.slack.com/

Illustration by Idalia Kulik.

This article was first published on Arne’s blog – Simplify C++! – on 19 October 2016 at https://arne-mertz.de/2016/10/passkey-idiom/

Arne Mertz has been working with modern and not-so-modern C++ codebases for over 15 years in embedded and enterprise contexts. He is a mentor and teacher for clean code and modern C++ for colleagues and customers at Zühlke Engineering.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.