Blag
He's not dead, he's resting
C++ Named Function Parameters
May 20, 2010
Posted by on C++ doesn’t have named function parameters. In some ways this isn’t a huge deal, since the compiler will usually catch when you screw up the ordering of arguments to a function. But if you’ve got a function accepting multiple arguments of the same type, the compiler isn’t going to save you. So we want to allow something like following:
shop.populate( param::number_of_cheeses() = 0, param::number_of_parrots() = 1, param::parrot_variety() = "Norwegian Blue" );
We also want:
- As little as possible boilerplate from the programmer.
- Type safety. It shouldn’t compile if the arguments are wrong.
- Zero overhead.
It would be nice to allow arguments to be specified in any order, and there is a way of doing that using C++0x, but it’s rather convoluted, so we’ll stick with the requirement that arguments be in the right order for now.
First, we want to work out the type of those param::foo()
things. Since we’re using operator=
, they need to be structs or constants of some kind (since operator=
can only be overloaded as a member function). Since we want lots of them of different types, and since we don’t want to have to worry about declaring the same name multiple times (which means we’d start hitting the ODR), a typedef
of a template seems in order. Thus, we’d like to do:
namespace params { typedef Name</* something */> number_of_cheeses; typedef Name</* something */> number_of_parrots; typedef Name</* something */> parrot_variety; }
As for the something, the best I’ve been able to come up with is an inline forward declaration of a meaningless struct
:
namespace params { typedef Name<struct N_number_of_cheeses> number_of_cheeses; typedef Name<struct N_number_of_parrots> number_of_parrots; typedef Name<struct N_parrot_variety> parrot_variety; }
What about the function parameters?
void Shop::populate( const NamedValue<param::number_of_cheeses, int> & number_of_cheeses, const NamedValue<param::number_of_parrots, int> & number_of_parrots, const NamedValue<param::number_of_cheeses, std::string> & parrot_variety) { /* ... */ }
There’s a small amount of duplication there, but that’s a necessity: it’s considered a useful feature of C and C++ that declarations and implementations of functions can use different names for parameters.
As for using the parameters, we’ve got two options. We could add a super magic cast operator to NamedValue
, or we could make it explicit. Since super magic casts have a nasty habit of doing really weird things, we’ll make it explicit using operator()
:
void Shop::populate( const NamedValue<param::number_of_cheeses, int> & number_of_cheeses, const NamedValue<param::number_of_parrots, int> & number_of_parrots, const NamedValue<param::number_of_cheeses, std::string> & parrot_variety) { cheeses.resize(number_of_cheeses()); cage.insert(number_of_parrots(), parrot_variety()); }
Now we just have to make it work. First, NamedValue
, remembering to provide const
and non-const
versions of our operator:
template <typename T_, typename V_> class NamedValue { private: V_ _value; public: explicit NamedValue(const V_ & v) : _value(v) { } V_ & operator() () { return _value; } const V_ & operator() () const { return _value; } };
Then Name
. Our first attempt might look like this:
template <typename T_> struct Name { template <typename V_> NamedValue<Name<T_>, V_> operator= (const V_ & v) const { return NamedValue<Name<T_>, V_>(v); } };
But there’s a problem: whilst this works for int
and most classes, it does something immensely stupid when fed a string literal. We could require users to write out parameters like:
param::parrot_variety() = std::string("Norwegian Blue")
but that’s rather silly. So instead we’ll add in a way of overriding types for NamedValue
, keeping it nice and generic in case any similar situations crop up elsewhere:
template <typename T_> struct NamedValueType { typedef T_ Type; }; template <int n> struct NamedValueType<char [n]> { typedef std::string Type; }; template <typename T_> struct Name { template <typename V_> NamedValue<Name<T_>, typename NamedValueType<V_>::Type> operator= (const V_ & v) const { return NamedValue<Name<T_>, typename NamedValueType<V_>::Type>(v); } };
Fortunately, g++
is smart enough to compile all of this into exactly the same code as it would if named parameters weren’t used.
And there we have it: very low boilerplate type safe named parameters with no icky macros.
“Fortunately, g++ is smart enough to compile all of this into exactly the same code as it would if named parameters weren’t used.”
You sure?
For certain values of g++, compile and exactly the same, yes.
Depending upon how you use it, there can be an extra copy in there, although that’s easily removed by a bit of tinkering.
What if you wanted to pass expensive-to-copy or polymorphic objects by reference?
Then you make use of tr1’s remove_reference and friends to make NamedValue work with references too. Not hard, but distracting from the main point.
Why not use boost?
http://www.boost.org/doc/libs/1_43_0/libs/parameter/doc/html/index.html
It’s been around for quite some time.
The Boost way of doing it is extremely convoluted, involving some rather horrible macros. BOOST_PARAMETER_FUNCTION looks nothing like a ‘regular’ function declaration… This way’s a lot simpler.
Plus there’s the usual Boost can of worms that means it’s pretty much impossible to use it for a system-critical package manager, which is the one thing I really care about.
Why not use the technique discussed in Design and Evolution of C++? That technique lets you reorder the parameters, and have default arguments for any of the parameters. It also involves less template magic.
struct ArgBlock {
int num_cheeses_;
int num_parrots_;
std::string parrot_variety_;
ArgBlock &num_cheeses(int x) {num_cheeses_ = x; return *this;}
ArgBlock &num_parrots(int x) {num_parrots_ = x; return *this;}
ArgBlock &parrot_variety(const std::string &x) {parrot_variety_ = x; return *this;}
};
void Shop::populate(const ArgBlock &args);
shop.populate(ArgBlock.
parrot_variety("Norwegian Blue").
number_of_parrots(1).
number_of_cheeses(0));
That’s an awful lot of work, which means I’d just end up not using it very often, leading to more screwups. It also doesn’t provide a way of detecting missing or duplicated parameters at build time.