
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); 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 { // 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;
// Define scope of lookup (some params may not be NTPs) typedef boost::mpl::vector<BufsizeOrNTP, ElemtypeOrNTP, StrideOrNTP, IterationsOrNTP> params; public: // Get param values typedef typename lookup<Bufsize, params, info>::type BufsizeP; typedef typename lookup<Elemtype, params, info>::type ElemtypeP; typedef typename lookup<Stride, params, info>::type StrideP; typedef typename lookup<Iterations, params, info>::type IterationsP; static void run(void) { // Set up and walk through arrays }; }; // Pentium III 256k cache #define CACHE_SIZE (256 * 1024) // Cached test (note out-of-order params) typedef read_bandwidth_test<Elemtype_is <int >, Bufsize_is <int, CACHE_SIZE/2>, use_default_for<Stride >, use_default_for<Iterations> > cached_test; // Uncached test typedef read_bandwidth_test<Elemtype_is <int >, Bufsize_is <int, CACHE_SIZE*2>, use_default_for<Stride >, use_default_for<Iterations> > uncached_test; // This works too (default for Stride and Iterations) typedef read_bandwidth_test<Elemtype_is <int >, Bufsize_is <int, CACHE_SIZE*2> > uncached_test_2; // And this (alternate interface, doesn't require BOOST_NTP macros) typedef read_bandwidth_test<param <Elemtype, int >, param_c<Bufsize, int, CACHE_SIZE*2> > uncached_test_3; // And this (default for Stride and Iterations) typedef read_bandwidth_test<use_default, param <Elemtype, int >, param_c<Bufsize, int, CACHE_SIZE*2> > uncached_test_4; // And this (positional + named binding) typedef read_bandwidth_test<integral_c<int, CACHE_SIZE*2>, param <Elemtype, int > > uncached_test_5; // And this (positional defaults, integral_c bound to Bufsize) typedef read_bandwidth_test<param <Elemtype, int >, integral_c<int, CACHE_SIZE*2>, use_default, use_default> uncached_test_6; template<typename Test> void run_test(void) { // Keep track of time and calculate stats, etc. } int main(void) { run_test<cached_test>(); run_test<uncached_test>(); run_test<uncached_test_2>(); run_test<uncached_test_3>(); run_test<uncached_test_4>(); run_test<uncached_test_5>(); run_test<uncached_test_6>(); return(0); } My goals with this interface are: - Allow arbitrary reordering of parameters - Unspecified parameters use defaults automatically, regardless of position of other parameters - Can use positional notation for binding and named mechanism arbitrarily. - Ease of use for user (*_is for readability, etc.) - Ease of use for developer (though see my notes below) 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.
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. 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 prefer the macroized version myself because you get nice notation like Bufsize_is<> for free. I stole that from the original iterator_adaptors library. ;)
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.
// 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. 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. In your scheme, how does the developer of the template class specify defaults in the template parameter list declaration? The user ought to be able to pass only those parameters she really wants to change. The one thing I don't like about my interface is that this specification is redudant with what's passed to key_info<>.
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. Can the user name all templates except one, put the unnamed template anywhere and have it bind correctly? 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. 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? 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. -Dave