Passkey Idiom: More Useful Empty Classes

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 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 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.

key

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.

Previous Post
Next Post
Posted in

8 Comments



  1. 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.

    Reply

    1. 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.

      Reply

  2. 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.

    Reply

    1. 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.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *