
"David A. Greene" <greened@obbligato.org> writes:
David Abrahams wrote:
I think if you look at what we're planning, maybe we can have a productive debate about what the best interface is.
I think that's a good idea. Here's what I have currently and I'll reply to your ideas after that.
A complete example of use, from developer (of some template class) and user (of that class) perspective. I've cut non-ntp implementations out to save space.
#include <boost/ntp/ntp.hpp> #include <boost/mpl/integral_c.hpp> #include <sys/time.h> #include <iostream>
// Declare named template parameters
BOOST_NTP (Elemtype); BOOST_NTP_C(Bufsize); BOOST_NTP_C(Stride); BOOST_NTP_C(Iterations);
I don't know what's under the covers of those macros; we've tried to make sure that there's a macro-less interface that's at least reasonably usable, so those would be: template <class T = int> struct Elemtype_is : parameter::template_keyword<a0_is<>, T> {}; I guess we don't have a keyword for non-type template parameters yet but the rest would be like: template <std::size_t n = 0> struct Bufsize_is : parameter::template_nontype_keyword<Bufsize_is<>, std::size_t, n> {}; How is your code dealing with the different types of nontype template parameters, e.g. std::size_t above?
using namespace boost::ntp; using boost::mpl::integral_c;
template<typename BufsizeOrNTP, typename ElemtypeOrNTP = default_<Elemtype, int >, typename StrideOrNTP = default_c<Stride, int, 1 >, typename IterationsOrNTP = default_c<Iterations, int, 100000> > class read_bandwidth_test {
Hm, interesting. What do these default_ things actually do? Isn't all the information after the first argument just ignored? Same question as above about non-type template parameters.
// Register all keys along with defaults (unfortunately redundant // with template parameter declarations above)
typedef key_info< boost::mpl::vector<Bufsize, default_ <Elemtype, int >, default_c<Stride, int, 1 >, default_c<Iterations, int, 100000> >
info;
Yeah, that is unfortunate. What's the point of the first set of declarations? We delay the the specification of defaults until the point of extraction. I suppose that has both upsides and downsides. Hmm; you're using mpl::vector -- are you doing anything to avoid O(N^2) instantiations for each parameter lookup?
// Define scope of lookup (some params may not be NTPs) typedef boost::mpl::vector<BufsizeOrNTP, ElemtypeOrNTP, StrideOrNTP, IterationsOrNTP> params;
In our case this would be typedef typename parameter::parameters< Bufsize_is<>, Elemtype_is<>, Stride_is<>, Iterations_is<> >::bind<BufsizeOrNTP,ElemtypeOrNTP,StrideOrNTP,IterationsOrNTP>::type params;
public:
// Get param values typedef typename lookup<Bufsize, params, info>::type BufsizeP;
And this is... a type? What is it, an mpl::int_<> ?
typedef typename lookup<Elemtype, params, info>::type ElemtypeP; typedef typename lookup<Stride, params, info>::type StrideP; typedef typename lookup<Iterations, params, info>::type IterationsP;
In our case (assuming we add support for non-type template parameters) this would be: typedef typename parameter::binding< params, Bufsize_is<> >::type BufsizeP; typedef typename parameter::binding< params, Elemtype_is<>, int >::type ElemtypeP; typedef typename parameter::binding< params, Stride_is<>, mpl::int_<1> >::type StrideP;
My goals with this interface are:
- Allow arbitrary reordering of parameters
- Unspecified parameters use defaults automatically, regardless of position of other parameters
I don't know what you mean by "automatically." I understand the second part of the sentence.
- Can use positional notation for binding and named mechanism arbitrarily.
I don't understand that at all.
- Ease of use for user (*_is for readability, etc.)
- Ease of use for developer (though see my notes below)
Doesn't look too easy for the developer. And IMO it would be better for the developer -- who is likely to want to use the parameter library anyway for function parameters -- to reuse his knowledge.
In one way or another, I've come across needs for all of these properties. The ability to arbitrarily reorder things and mix positional and named parameters is particularly useful when working with existing code. NTPs can be added incrementally without too much trouble.
The parameter library now supplies that. Additionally it has support for "unnamed" parameters (those whose identity can be deduced from the type passed) and the specification of MPL lambda expressions to identify the legal range of types for each parameter (with an error generated if required parameters can't be found).
A user might do something like this:
namespace parameter = boost::parameter;
// Declare a named template parameter wrapper template <class T = void> struct value_type_is : parameter::named_template_parameter<value_type_is<>, T> {}
This is what I use BOOST_NTP for. Of course it doesn't really matter what the underlying code looks like unless the user doesn't want to use macros for some reason.
It does matter. You're introducing declarations and definitions into the user's code. He needs to know what's going on.
In the latter case, the interface I have allows the user to declare a named template parameter like this:
strict Bufsize {}; ^ :)
The param<> notation at use then tells the NTP engine that this is a named template parameter.
I see no suuch notation.
I prefer the macroized version myself because you get nice notation like Bufsize_is<> for free. I stole that from the original iterator_adaptors library. ;)
Yes; we could add a macro easily enough.
template <class A0, class A1, class A2> struct foo { typedef parameter::parameters< value_type_is<> // more parameters listed here > foo_parameters;
So far, this looks exactly like my interface.
typedef typename foo_parameters::bind<A0,A1,A2>::type args;
Here we differ. You use bind<> while I use key_info<>.
I prefer the bind<> notation but I like the fact that key_info<> keeps parameters and defaults together in one place.
But it doesn't; the defaults are repeated and thus spread out.
// Extract the value_type; the default is int typedef typename parameter::binding< args, value_type_is<>, int >::type value_type; };
So in my interface the programmer specifies defaults at declaration time while in yours she does it at lookup time. I think I like yours better.
Haha! If there was one area in which I was prepared to consider that our interface could be improved on, it was that part.
I'm trying to think of whether there are advantages of one over the other. A possible one is that in my scheme the programmer cannot accidentally specifiy two different default values for an NTP. In yours one could conceptually have two typedefs for value_type_is<>, one with a default of int and another with, say, float. I don't know why anyone would do this, though. I don't think it's a big deal.
Agreed.
In your scheme, how does the developer of the template class specify defaults in the template parameter list declaration?
He passes parameter::void_. We're thinking of renaming that parameter::not_specified.
The user ought to be able to pass only those parameters she really wants to change.
Of course; that's supported. See libs/parameter/test/ntp.cpp
The one thing I don't like about my interface is that this specification is redudant with what's passed to key_info<>.
Yeah; why bother?
foo<float, .., ..>::type == float foo<value_type_is<float>, .., ..>::type == float
I'm not sure what you're trying to convey here. It looks like you have both positional and named binding, which is good.
Yes.
Can the user name all templates except one, put the unnamed template anywhere and have it bind correctly?
Yes. But let's call those "positional." In our terminology "unnamed" means something else (see above).
In my implementation, the user could specify one named parameter and three unnamed ones and the unnamed parameters will bind in the order of declaration, even though positionally they may not "align" with the declaration.
Ours works differently IIRC; positional parameters bind strictly by position. But I don't see this as being very important, though. Most languages that allow this sort of thing natively require that all positional parameters precede the named ones anyway and I don't see why a user would want to do otherwise.
For example:
read_bandwidth_test<int, integral_c<int, 1>, integral_c<int, 100000>, Bufsize_is<int, CACHE_SIZE*2> >
would bind Elemtype to int, Stride to 1 (mpl constant) and Iterations to 100000.
The beauty of this is that it will re-use all of the existing capabilities of the library, like positional and unnamed arguments (those that can be distinguished based on their properties and so don't need to be tagged explicitly).
I still don't completely understand unnamed arguments. Does my last example above cover what you mean?
No, please see http://www.boost.org/libs/python/doc/v2/class.html#class_-spec for an example. This predates the parameter library; if I can find the time I'm going to go through Boost.Python and parameter-ize everything.
The first three parameters are not named but the naming of the last one defines the binding order for the others.
I'm happy to help with an implementation based on named_parameters but as before I will need some documentation of internals so I can understand the code.
I hope you don't feel your efforts have gone to waste, but I don't think there's much left to do. From my point-of-view, it was not a waste at all because it prodded us to get the NTP feature done in Boost.Parameter. Daniel tells me you wrote over 400 tests for your library! It might be a great contribution if the logic of those tests could be reworked to test Boost.Parameter instead. At the very least we'd discover if we were missing anything important provided in your work. -- Dave Abrahams Boost Consulting www.boost-consulting.com