|
Boost : |
From: Gennadiy Rozental (gennadiy.rozental_at_[hidden])
Date: 2004-11-16 03:02:30
Hi,
Recently I became quite fond of interfaces with named parameters. I
developed variety of different solutions with different trade-off and set of
features. At first here is couple notes from my experience:
1. In most cases multiargument interface are not in high performance
critical areas.
2. In many cases it's constructors.
Now lets delve in more detailed discussion on:
Design
===============
I. Active vs. passive
I found that there are two major branched in designs for named parameters:
"active" parameters collection and "passive" one.
Submitted library is a variation of "passive" solution: one need to call
"get" method to retrieve any parameter from collection:
template<typename Params>
void
foo( Params const& p )
{
std::string name = p[name];
std::string description = p[descr];
...
}
An "active" solutions automatically place parameters in appropriate slots by
single 'apply' call:
class A {
public:
template<typename Params>
A( Params const& p ) { p.apply( *this ); }
};
Even though "active" solutions in some cases provide an interface look more
natural. I found that "passive" solution in most cases more flexible and
easier to implement. While in both cases one may employ runtime
polymorphism, with "passive interfaces it's more natural. Note that
parameter may not necessary is just a simple value, it maybe some kind of
optional feature and 'apply' method maybe not so trivial and even invoke
some functions. All in all both design seems to have heir advantages and I
believe we need to provide support for both flavors.
II Interfaces
a) Call interface and parameters combining
IMO with some very rare exclusions (1/2 optional parameter) the named
parameter framework shouldn't require/expect multiple overloading of target
function with different number of parameters. As well as any "impl"
functions. The only acceptable interface is the one with single Params
template parameters (in some cases one may provide positional interface as
well, though it's rather exclusion case). This is applicable both to active
and passive solutions:
class A {
A() : ... {}
template<typename Params>
A( Params const& p ) : m_name( p[name] ), m_gulp( p[gulp] ) {}
};
This immediately bring a need for automatic arguments combining. There are
several ways to do so, depending on requirements (BWT I liked interface
presented in some other post with using ',' as combine operator).
b) Optional parameter support
In case of active solutions there should be an interface to check presence
of specific argument. Submitted solution will cause compile time error on
access to non-supplied parameter. This may not be always most convenient and
may not be the case at all with different implementation (runtime based)
c) Default value support
I specifically dislike an interfaces provided to support default values.
They are unreasonably cryptic (who would remember difference between | and
||; and why | ) but also misleading since default value is actually with
access brackets: what is p["abc"]? There are several alternatives. My
personal preference is :
int name_ = p.is_present<name> ? p[name] : "abc"; // you will need some MP
trick to make it compilable
or
int name_ = p.is_present( name ) ? p[value] : "abc"; // this assumes runtime
based implementation
Active solutions doesn't have such issue. Whatever is there gets set by
apply call.
d) Type safety and restrictions
I do not understand why wouldn't we keep data type somewhere around keyword
type and require one to supply specific type during invocation.This library
instead invented special mechanism of imposing restrictions. I do not
believe we need that (Also my guess is that 8 out of 10 developers
unfamiliar with library will be left spellbound facing this code)
e) Mix of named and positional parameters
I don't believe it's actually a good idea to support such mix. IMO it just
adding unnecessary confusion and possible source of users errors.. Let's say
I expect 2 parameters: 1. string name 2. int value. call foo( value = 5,
"my_name" ) will fail, while foo( "my_name", value = 5 ) and foo( value =
5 ) won't. Too much chances to make mistake IMO.
III. Implementation
IMO whatever design you prefer implementation shouldn't as complex and
cumbersome. Named parameters support is pretty simple task. This submission
presents 1000 lines of tight MP code (and this is without numerous
includes). In majority of the cases much more simple and straightforward
solution is possible (with some acceptable IMO tradeoff). Following 80 lines
(I think it's possible to make it even smaller would I use mpl) should
basically present solution with similar tradeoff as submitted library
(without default value support, but IMO it would take another 40-60 lines,
just save it into keyword struct):
// Library Code
template<typename NP1,typename NP2>
struct named_parameter_combine;
template<typename T, int unique_id>
struct keyword;
template<typename Derived>
struct named_parameter_base
{
template<typename NP>
named_parameter_combine<NP,Derived>
operator,( NP const& np )
{
return named_parameter_combine<NP,Derived>( np,
*static_cast<Derived*>(this) );
}
};
template<typename T, int unique_id>
struct named_parameter
: named_parameter_base<named_parameter<T, unique_id> >
{
static const int id = unique_id;
named_parameter( T const& v ) : m_value( v ) {}
T const& operator[]( keyword<T,unique_id> const& ) const { return
m_value; }
T const& m_value;
};
template<bool>
struct selector;
template<typename NP1,typename NP2>
struct named_parameter_combine
: named_parameter_base<named_parameter_combine<NP1,NP2> >
{
named_parameter_combine( NP1 const& np1, NP2 const& np2 )
: m_np1( np1 ), m_np2( np2 )
{}
template<typename KW>
typename KW::data_type const& operator[]( KW const& kw ) const
{ return selector<KW::id==NP1::id>::_( m_np1, m_np2, kw ); }
NP1 const& m_np1;
NP2 const& m_np2;
};
template<>
struct selector<true>
{
template<typename NP1,typename NP2,typename KW>
static typename KW::data_type const& _( NP1 const& np1, NP2 const& np2,
KW const& kw )
{
return np1[kw];
}
};
template<>
struct selector<false>
{
template<typename NP1,typename NP2,typename KW>
static typename KW::data_type const& _( NP1 const& np1, NP2 const& np2,
KW const& kw )
{
return np2[kw];
}
};
template<typename T, int unique_id>
struct keyword
{
typedef T data_type;
static const int id = unique_id;
named_parameter<T,unique_id>
operator=( T const& t ) const
{
return named_parameter<T,unique_id>( t );
}
};
////////////////////////////////////////////////////////////////
// Example:
namespace test
{
typedef char const* c_str;
keyword<c_str,1> name;
keyword <float,2> value;
keyword<int,3> index;
double value_default()
{
return 666.222;
}
int foo( c_str name, float v, int i )
{
}
template<class Params>
int foo(Params const& params)
{
foo( params[name], params[value], params[index] );
}
}
int main()
{
using test::foo;
using test::name;
using test::value;
using test::index;
foo(( index = 20, name = "foo", value = 2.5 ));
return 0;
}
Strict type checking on top of it. This code works under gcc 3.4.2. Should
work everywhere.
As you may guess by now my vote is NO to accept this library. I personally
wouldn't be using it in current form and IMO as it stands now it's just
another collection of neat MP tricks.
I do not have to much comments on docs and tests, other then they seems
incomplete.
Regards,
Gennadiy
P.S. Why is the RC branch of mpl is different from HEAD? Doesn't we supposed
to fix HEAD and only then propagate to branch?
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk