|
Boost : |
From: Andrei Alexandrescu (andrewalex_at_[hidden])
Date: 2002-04-19 16:33:16
I found many of Fernando's arguments insightful; as he said, they mostly
relate to misusing policies as an interface device (rather than an
implementation device). Please bear with me.
> The first thing I noticed that would happened if I used a policy *as an
> additional template parameter*, that is, an "intrusive policy", is that
>
> optional<A,Policy1> and optional<B,Policy2> become distinct types.
Just a nit: optional<A> and optional<B> are distinct types from the get-go,
so that doesn't mark any difference.
> In the case of optional<>, it is clear to me that this is a mistake,
because
> these Policies describe an implementation detail of optional<> (whether it
> should /shouldn't bypass A's def ctor), so they souldn't be part of the
> static-type.
I agree with that. When the design decision is bound to the type, depends
solely on it, and is unlikely to change, it is best that that decision is
made through a trait.
> In the case of smart_ptr<> however, it could be argued that a given policy
> must be accounted as part of the static-type of the smart pointer.
My point exactly :o).
> Scenario 1: I want to write a class which does something with a given
> source object. This class needs to keep the object alive, but it is quite
> possible that the caller disposes the object even before this class
> completes its task. This scenario calls for a common pattern:
>
> class Processor
> {
> Processor ( smart_ptr<A> const& obj ) : m_obj(obj) {}
> ...
> smart_ptr<A> m_obj ;
> } ;
>
>
> The important thing in this example is that all I really know of is A, and
> that I need it to be alive as long as Proccesor needs it.
> If smart_ptr<> is intrusively-policy-based, I -the user of A- must make
> decisions about which of those policies would fit here. Some of those
> decisions might be hard to take on this side of the system, but even if
can
> take them, there is still this problem:
I understand. So the problem has two aspects (1) you don't want Processor to
make policy decisions on how smart pointers to A should look like, (2) you
don't want to add clutter (in form of template arguments) to Processor to
have it support various policies that smart_ptr might have.
This is the typical example where a template typedef would be extremely
useful. Again, policies are only an implementation device: they help you
with not having to write various flavors of smart pointers from scratch.
Absent typedef templates, one can use (1) a straight typedef smart_ptrA
that's used as an interface device for passing smart pointers to A's all
around, or (2) the "nested typedef" trick:
template <class T> struct SmartPtr
{
typedef Loki::SmartPtr<T, ...> Type;
};
Then you implement the communication between various components by using
SmartPtr<T>::Type.
The alternate design you mention is to customize smart_ptr with traits. This
is quite an attractive design, especially given that you can define traits
for a whole hierarchy (or sub-hierarchy) in a single shot - no duplication!
See http://www.moderncppdesign.com/publications/traits.html and
http://www.moderncppdesign.com/publications/traits_on_steroids.html.
There are things that work best with traits and things that work best with
policies. Please allow me to do a short comparative analysis below.
1. A trait binds a type, or a (sub)hierarchy, to a capability. That binding
is not optional and irrefutable. You can provide a default capability for
types you know nothing about.
An important, well, trait of traits [:o)] is that you cannot define traits
for types you know nothing about. Instead, you define an interface for the
capability abstracted by the trait, provide maybe a default, and have users
to define their own traits if they need to.
In the example of SmartPtr, one can argue that for a given type, only one
ownership method will ever be used throughout the application. For example,
if you have COM types, you'll never use *anything* but COMRefCounting for
those types. If you have CORBA types, you'll never use *anything* but
CORBARefCounting for those types. If you have... you get my drift. This
would suggest that ownership is best made a trait (likely a hierarchy-wide
trait) and not a policy. However, imagine you have a Widget object. Some
smart pointers to Widgets are copied between threads, some are not. It is
reasonable to aim at a design that incurs the MT overhead only when needed.
So in this case, the traits-based approach falls short because it cannot
define two traits for the same Widget type.
Continuing the SmartPtr study, let's move on to the checking policy. Is that
at best a trait, or not? Practical experience suggests it's not, this time
for a slightly different reason.
Many times, checking might be a system (or subsystem) wide decision. That
is, you compile a whole module with "null dereference checking" on, and you
know that that subsystem will fail safely whenever that particular problem
occurs. So in this case, what checking method to use transcends multiple
types and hierarchies. Again, the traits-based approach falls short because
(1) different parts of the same application might want to use different
checking methods for the same type; (2) you want to change that capability
once in a shot, and not for every type. You can do the latter with traits,
with enough awkwardness to strengthen the argument against its
effectiveness.
On to the Storage policy. Is storage bound to type, or not? The primary
purpose of Storage is to give the SmartPtr user a crack at defining
alternate represenations, a custom default value, and on-the-fly
initialization. It also allows the user to define proxies by having
stored_type be itself a sort of smart pointer. I didn't really experiment a
lot with custom storages, but my intuition says that sometimes you don't
want to bind all that to a specific type.
2. Policy-based design allows you to statically bind design decisions to the
designs that you make. This approach is very natural for many designs. As I
will show shortly, while you cannot simulate policies with traits, you can
do with policies everything you do with traits, and much more.
Policies are best used instead of traits when you want to want to implement
the same capability of a design over a range of types, or when you want to
use different design capabilities of an artifact with the same type.
Let's get back to the SmartPtr multithreading analysis. In that case, it is
best to use a policy-based design and use SmartPtr<Widget, RefCounting> and
SmartPtr<Widget, MtRefCounting>. The fact that these are different types is
actually quintessential to the well-functioning of the design. It is under
programmer's control /if/ and /how/ smart pointers with different
capabilities are copied to each other.
If you do want to bind a capability to a specific type or hierarchy, you can
do that with policy-based design. All you need to do is to define a policy
and specialize it appropriately for the types or hierarchies that you need.
For example, consider you want to use COM-style refcounting for COM objects
and straight refcounting for all others.
template <class T> struct AutoRefCounting
{
typedef Select<is_base_derived<IUnknown, T>::result,
COMRefCounting,
RefCounting>::Result;
};
Then you can specialize AutoRefCounting to define specific reference
counting capabilities for specific types or hierarchies. I believe this is a
good approach, and typedef templates will only make it much more
comfortable.
> Scenario 2:
>
> It is quite common to specialize certain things -such as predicates- on a
> smart_ptr<>. This is in order to allow certain expressions to be applied
> indistinctely to bare and smart pointers.
> For example, the boost shared-ptr declares:
>
> // get_pointer() enables boost::mem_fn to recognize shared_ptr
> template<typename T> inline T * get_pointer(shared_ptr<T> const & p) ;
>
> This is used -as the comment says- inside mem_fn which allows a very
> powerful usage of mem_fn.
>
> If shared_ptr were intrusively policy-based, this wouldn't be feasible
> becuase mem_fn couldn't know about the smart pointer policies.
Because the example lacks enough context, I cannot comment on it. Maybe in
light of the comparison above, a solution will naturally pop up.
> Just as is the case with optional<>, a smart pointer is a library fature
so
> general that the ability to express a smart pointer as a single-parameter
> template class: smart_ptr<T> is so powerful that directly competes with
the
> power of the flexibility given by a policy-based class.
I disagree with that.
> Dietmar already shown -and I totally agree- that most of the smart ptr
> policies are usually type dependent, so they perfectly call for external
> policies. (BTW, the sketch made by Doug seems very reasonable).
As shown above with examples, I believe the precise contrary. The examples I
gave clearly show that all policies might need to be decoupled from types.
> The problematic case is the threading policy which would certainly loose
too
> much of its flexibility if it is specified externally in a global basis,
as
> Andrei warned.
The problematic case is when you misuse policies.
> IMHO, Dietmar has raised very strong points.
>
> I believe that most of the important policy choices could be made
> externally, based only on the type. This will cut-down a lot of the
> combinatorial possibilities, but, IMO, will still give a significant
degree
> of flexibility while allowing the system to retain the smart_ptr<T> simple
> signature which I've shown (I hope) to be fundamental.
IMHO, the points raised are invalid. The discussion starts from an example
of misuse of policies, and furthermore, does not propose a better design, as
I tried to show above.
There is nothing fundamental about smart_ptr's simple signature (whatever
that means). Template typedefs are not a fundamentally new addition to the
languages, and even their simulation in today's C++ renders the points moot.
Andrei
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk