23 Apr, 2013

True Story: When Friendship Smothers

Episode Three tells us about friendship, and how it can be used to narrow access to a class or function. However, that's not the only thing friendship restricts, it also restricts how we may use such class or function. What follows is a concrete example of how friendship may cramp your style.

The service & session situation

While working on a service, any kind of stateful service, there is often the need to represent its sessions as separate objects. Such sessions would be aggregates of all the necessary information needed to represent a service session, which is entirely tied to the implementation of the service they are part of. The coupling introduced by this particular use of friendship will not be an issue; sessions are, in a way, subobjects of the service as they can only exist within a service.

class service;

class session
{
  friend service;

  session( session const& ) = delete; // non-copyable
  session( session&& ) = delete; // non-movable
  :::
};

A session is not copyable, copying simply makes no sense. It isn't movable either, not because it follows from the design but for external constraints —lets just say it holds an object of a non-movable type, like std::mutex—.

Construction

Given that a session can only be created by a service, it appears to make sense to set it up with a private constructor.

class session
{
  :::
private:
  session( service& s ){ ::: }
  :::
};

The only one that can create sessions —besides session itself—, is its one friend the service.

class service
{
  :::
  void new_connection()
  {
    auto s = new session( *this );
    /** stash the new session somewhere... **/
  }
  :::
};

What about utility functions?

But maybe you have read Herb Sutter's advice on exception-safe function calls —and if you haven't, you should—, and you know that a raw pointer is not the best tool for the job. A unique_ptr would be a better choice.

class service
{
  :::
  void new_connection()
  {
    auto s = make_unique< session >( *this );
    /** stash the new session somewhere... **/
  }
  :::
};

And then the darkness: make_unique cannot construct a new session, only a service can do that. In order for make_unique to be able to construct a new session, it would have to be one of its friends too.

class session
{
  :::
  template<typename T, typename ...Args>
  friend std::unique_ptr<T> make_unique( Args&& ...args );
  :::
};

This in fact works —at least for now— but it doesn't solve the problem, it just pushes it a bit further. Note that make_unique is unqualified, as it is not yet part of the language. It is/would be the unique_ptr counterpart to shared_ptr's std::make_shared. Quoting the referred Guru of the Week article:

That C++11 doesn’t include make_unique is partly an oversight, and it will almost certainly be added in the future. In the meantime, use the one provided below.)

template<typename T, typename ...Args>
  std::unique_ptr<T> make_unique( Args&& ...args )
  {
    return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
  }

C++14 draft has become ready since the time that article was posted, and make_unique is now part of the standard library. Quoting from ISO C++ Spring 2013 Meeting:

One of the smallest additions is actually great in its impact. It’s make_unique:

auto u = make_unique<some_type>( constructor, parameters, here );

The reason make_unique has important impact is that now we can teach C++ developers to mostly never use explicit new again. In C++11 we already could teach to never use raw pointers and explicit delete again, except in rare cases that are hidden inside a class in order to do something like implement a low-level data structure. However, we could not teach to never write new because although make_shared was provided to create a shared_ptr, new was still needed to create a unique_ptr. Now, instead of “new”, write make_unique or make_shared.

A note on lambdas

Lambda functions allows an anonymous function object to be defined in place. They are not just syntactic sugar, they are a special construction. A particular property of these constructions is that the body of a lambda function is considered as if it were in the context where the lambda is being introduced. This means, among other things, that code within a lambda function is granted the same level of friendship than code right outside of it.

[5.1.2/7] The lambda-expression’s compound-statement yields the function-body (8.4) of the function call operator, but for purposes of name lookup (3.4), determining the type and value of this (9.3.2) and transforming id-expression s referring to non-static class members into class member access expressions using (*this) (9.3.1), the compound-statement is considered in the context of the lambda-expression. [ Example:

struct S1 {
    int x, y;
    int operator()(int);
    void f() {
      [=]()->int {
        return operator()(this->x + y); // equivalent to S1::operator()(this->x + (*this).y)
        // this has type S1*
      };
    }
  };

—end example ]

What about utility functions' utility functions?

There is no guarantee nor requirement that a standard make_unique would have to be as simple as our provisory implementation is; as there isn't such requirement in the standard now for make_shared. Let's assume that a session will take care of its own lifetime, instead of having the service take care of it, and move to using shared_ptrs instead.

class session : std::enable_shared_from_this< session >
{
  :::
  template<typename T, typename ...Args>
  friend std::shared_ptr<T> std::make_shared( Args&& ...args );
  :::
};

class service
{
  :::
  void new_connection()
  {
    auto s = std::make_shared< session >( *this );
  }
  :::
};

This no longer works —not in a portable way—, even though we have befriended the right function. The actual construction of the new session can potentially be done within any other function directly or indirectly called by make_shared. It is just the same problem initially shown for our provisonary make_unique, except that we no longer have control over it.

What about optimizations?

So it seems that the only safe decision would be to resign to any utility function and have the service be the one that creates the session, since its the only one allowed to.

class service
{
  :::
  void new_connection()
  {
    auto s = std::shared_ptr< session >( new session( *this ) );
  }
  :::
};

This once again works, and it seems to be doing the same thing we intended before. But it isn't. This approach performs two memory allocations: one for the explicit new in the code, and one for the internal reference count of the shared_ptr within its constructor. The make_shared approach, on the other hand, will allocate a single block of memory in which it will keep both the internal reference count and the newly constructed object. Here is the relevant paragraph of the standard regarding make_shared:

[20.7.2.2.6/6] Remarks: Implementations should perform no more than one memory allocation. [ Note: This provides efficiency equivalent to an intrusive smart pointer. —end note ]

This also allows a second optimization that reduces the control block by the size of one pointer. Stephan T. Lavavej calls it the We Know Where You Live optimization, which explains at his STL11: Magic && Secrets panel on GoingNative 2012.

(A) Solution

The problem started when we gave session a private constructor, as they can only be created by services. Actually, we don't care who does create a new session, as long as it is on behalf of a service. The simplest solution would be to make the constructor public, and make it an implementation detail.

namespace detail {
  class session : std::enable_shared_from_this< session >
  {
  public:
    session( service& s ){ ::: }
    :::
  };
} // namespace detail

class service
{
  :::
  void new_connection()
  {
    auto s = std::make_shared< detail::session >( *this );
  }
  :::
};

The compiler will no longer restrict access to session, but placing it in a detail namespace should signal the user this is an implementation detail —impl is another name commonly used—. We have changed a contract with the compiler for one with the user, which we cannot enforce. This is an acceptable solution since we are guarding against Murphy, not Machiavelli.

If we wish to have the compiler enforce this new contract, we can use a combination of the approaches introduced in the previous episode. We will split a session into its public interface and the interface available for the service to consume; and we will use an access_key so that the construction of a session can be indirectly performed when a service requests it.

class session : std::enable_shared_from_this< session >
{
  :::
protected:
  session( service& s ){ ::: }
  :::
};

namespace detail {
  class session_impl : session
  {
  public:
    session_impl( service& s, access_key< service > ) : session{ s } { ::: }
    :::
  };
} // namespace detail

class service
{
  :::
  void new_connection()
  {
    /** only a service can create an access_key< service >, but anyone can copy or move it afterwards **/
    auto s = std::make_shared< detail::session_impl >( *this, access_key< service >{} );
  }
  :::
};

Comments

#1rhalbersma16 May, 2013

Here's a code fragment that actually protects against Machiavelli as well: http://ideone.com/eCrNti It uses a nested class that takes an access key from the Service class. This way, the constructor of Session can be made private rather than protected (which would leave open the backdoor for other classes deriving from Session, and bypassing the access key).

#2K-ballo17 May, 2013

@rhalbersma: The Key in your code sample should have a private default-constructor so that only objects of type T can construct it. Note that an implicitly generated default-constructor is public.

#3Sebastian22 Jun, 2013

What STL calls "We Know Where You Live" is not the general fused allocation of make_shared. Instead, it's an implementation detail of the shared_ptr where you can make the control block one pointer smaller in the case of make_shared.

Generally speaking, a shared_ptr contains two pointers: one to the object it references, and another to the control block. The control block contains the strong reference count (object is destroyed when this goes to zero), the weak reference count (control block is destroyed when this goes to zero), the original pointer that was passed to the shared_ptr constructor (needed to implement heterogeneous shared_ptr aliasing, e.g. a shared_ptr<Base> and a shared_ptr<Derived> pointing to the same object), and finally the deleter.

Two optimizations can be applied. If the deleter is stateless, it can be omitted. And the other is the "We Know Where You Live" optimization: if make_shared was used, the object has been allocated together with the control block, i.e. you can find its address simply by performing pointer arithmetic on the control block's address. This way, you don't need to store the pointer.