====== 2012.12.28 - named parameter ======
many languages have support for named parameter. that is you can type something like:
size(width=42, height=13)
instead of unintuitive (when reading code) //size(42,13)//... wait -- or was it //size(13,42)//? you get the point. there is one more nice use case -- providing non-default value for a single parameter, out of many with default values. that is:
connect(user, pass, host="localhost", ssl=false, keepAlive=false);
now try connecting with this function, changing only SSL to be set to true.
===== boost named parameters =====
C++ lacks support for named parameters, though (as usual) it can be overcome. [[http://www.boost.org/doc/libs/1_37_0/libs/parameter/doc/html/|boost named parameters]] library does that using macros and setters. example syntax is:
// some declarations goes here...
func(42, _my2=MyClass{});
the problem is that arguments are always copied. this is not nice, when you're passing bigger elements, like user-defined classes, that may have non-inline, non-trivial copy c-tor.
===== variadic templates solution =====
recently i've came to the conclusion that the same can be done suing variadic templates and perfect forwarding.
==== general idea ====
the basic idea is to get all c-tor arguments as a variadic template arguments and then use extractor, that will return proper argument from all arguments provided. argument extraction is then calling extractor with different argument, to extract different values. example code may look like this:
template
struct extract
{
template
explicit extract(T t): default_{t} { }
E from(void) { return default_; }
template
E from(E e, Tail...) { return e; }
template
E from(Head, Tail...t) { return from(t...); }
private:
E default_;
};
// ...
class Conn
{
public:
struct Host { std::string value_; };
struct User { std::string value_; };
struct Pass { std::string value_; };
struct UseSSL { bool value_; };
struct RetryCount { unsigned value_; };
template
explicit Conn(Args...a):
host_( extract("my.db.org").from(a...).value_ ),
user_( extract("john").from(a...).value_ ),
pass_( extract("doe1940").from(a...).value_ ),
useSSL_( extract(false).from(a...).value_ ),
retCnt_( extract(10u).from(a...).value_ ),
my_( extract(MyClass{}).from(a...) )
{ }
// ...
private:
decltype(Host::value_) host_;
decltype(User::value_) user_;
decltype(Pass::value_) pass_;
decltype(UseSSL::value_) useSSL_;
decltype(RetryCount::value_) retCnt_;
MyClass my_;
};
above example works fine, but:
- it does a lot of unneeded copies.
- it always created default value, that sometimes is overwritten.
- it does not check for errors (missing argument, argument given more than once).
==== down the rabbit hole ====
to get rid of extra copies perfect forwarding can be used. default argument need not be created, unless it is needed (i.e. no explicit, user-provided value is given) -- lambda function can be used to create one for us! finally error checking can be done, by ensuring that each argument (when extracted), exists only in one instance. for checking for lack of argument, when no default value is provided, special type can be used.
=== improved extractor ===
our new extract can look like this:
/** @brief type signalling that there is no default value present for a given parameter. */
struct NoDefaultValue {};
/** @brief extractor template - gets the value of a given paramter. */
template
struct extract
{
/** @brief extracts type E from the Args argument list, default value or compilation error. */
template
static E from(Args&&...args)
{
ensureUnique( std::forward(args)... ); // sanity check
return fromImpl( std::forward(args)... ); // actuall implementation
};
private:
/** @brief short for remove_cv and decay on a given type. */
template
struct Raw
{
typedef typename remove_cv::type>::type type;
};
//
// counts number of occurences of given type C in the argument list.
//
template
static constexpr size_t count(void)
{
return (is_same< typename Raw::type,
typename Raw::type >::value?1:0)
+
count();
}
template
static constexpr size_t count(void)
{
return 0;
}
//
// extraction itself
//
/** @brief template used for signalling missing parameter of a given type. */
template
constexpr bool missingParameter(void) { return false; }
/** @brief causes compilation error when required parameter is not supplied. */
static E fromImpl(NoDefaultValue&&)
{
static_assert( missingParameter(), "missing paramter with no default value: " );
}
/** @brief created default agrument, using user-provided functor. */
template
static E fromImpl(F&& f)
{
return f();
}
/** @brief extractor of a given paramter, when it has been found. */
template
static
typename enable_if< is_same< typename Raw::type, E >::value, E >::type
fromImpl(Head&& e, Tail&&...)
{
return std::forward(e);
}
/** @brief general search - no proper value found yet. */
template
static
typename enable_if< !is_same< typename Raw::type, E >::value, E >::type
fromImpl(Head&&, Tail&&...t)
{
return fromImpl(std::forward(t)...);
}
//
// wrapper call that ensures all arguments are unique.
//
template
static void ensureUnique(Head&& head, Tail&&...tail)
{
static_assert( count() <= 0, "argument is repeated" ); // check current one
ensureUnique( std::forward(tail)... ); // check next
};
static void ensureUnique(void) { }
};
class NoDefaultValue is passed as the last argument to the extractor, to signal end of arguments, while no default value is provided. it is provided explicitly, since otherwise last argument is expected to be lambda, that creates default value, if it is available.
private section contains //Raw// class, that is a simple wrapper to get raw type (no CV, references, etc...) -- they make type matching difficult, this are not welcomed when checking types, though cannot be simply removed, since lvalue/rvalue difference is crucial for perfect forwarding functionality. this is why //enable_if// does appear in code few times, to make sure compiler uses call that we want it to use.
//count// methods are used to count number of instances of a given type in the argument list. it is then used in the //ensureUnique// method to do sanity checks for all arguments.
whole implementation consists of //fromImpl// methods. the interesting part is usage of //missingParameter// template, that makes static assertion failed, when no explicit value is given, while default is not provided. this stops compilation, but //missingPameter// must be a template, so that it will not be instanciated, unless error-signaling //fromImpl// is requested.
=== wrapper macros ===
to make it all look a bit simpler on the user-end wrapper macros are provided:
/** @brief declares c-tor of the class with the named parameters. */
#define BNP_CONSTRUCTOR(name) template explicit name(Args&&...a)
/** @brief declares new paramter type. */
#define BNP_MK_PARAM(name, type) struct name { type value_; operator type&&(void) { return std::move(value_); } }
/** @brief gets the underlying type from the parameter wrapper. */
#define BNP_PARAM_TYPE(type) decltype(type::value_)
/** @brief extracts given paramter via the type. */
#define BNP_EXTRACT(type) extract::from( std::forward(a)..., NoDefaultValue{} )
/** @brief the same as BNP_EXTRACT but with the explicitly provided default value. */
#define BNP_EXTRACT_D(type, defVal) extract::from( std::forward(a)..., [](){return type{defVal};} )
/** @brief the same as BNP_EXTRACT but informing that the default c-tor is to be used for default value. */
#define BNP_EXTRACT_DC(type) extract::from( std::forward(a)..., [](){return type{};} )
=== example class declaration ===
having this done, usage is really simple:
class Conn
{
public:
BNP_MK_PARAM(Host, std::string);
BNP_MK_PARAM(User, std::string);
BNP_MK_PARAM(Pass, std::string);
BNP_MK_PARAM(UseSSL, bool);
BNP_MK_PARAM(RetryCount, unsigned);
// NOTE: no declaration for user's unique type
BNP_CONSTRUCTOR(Conn):
host_( BNP_EXTRACT_D(Host, "my.db.org") ),
user_( BNP_EXTRACT_D(User, "john") ),
pass_( BNP_EXTRACT_D(Pass, "doe1940") ),
useSSL_( BNP_EXTRACT_D(UseSSL, false) ),
retCnt_( BNP_EXTRACT_D(RetryCount, 10u) ),
my_( BNP_EXTRACT_DC(MyClass) )
{ }
// ...
private:
BNP_PARAM_TYPE(Host) host_;
BNP_PARAM_TYPE(User) user_;
BNP_PARAM_TYPE(Pass) pass_;
BNP_PARAM_TYPE(UseSSL) useSSL_;
BNP_PARAM_TYPE(RetryCount) retCnt_;;
MyClass my_;
};
=== usage ===
now usage is trivial -- all one have to do is to create arguments by type, and pass them to the function:
Conn c1; // ok - all default
Conn c2( Conn::UseSSL{true}, Conn::User{"alice"}, Conn::Pass{"$3cr37"} ); // overwrite some
Conn c3( Conn::Host{"other.db.host.org"} ); // ...
Conn c( Conn::Host{"other.db.host.org"}, MyClass{} ); // provide user's arg
MyClass mc1;
Conn c( Conn::UseSSL{true}, mc1, Conn::User{"alice"}, Conn::Pass{"$3cr37"} ); // copy user's arg
const MyClass mc2;
Conn c( Conn::UseSSL{true}, mc2, Conn::User{"alice"}, Conn::Pass{"$3cr37"} ); // copy const user's arg
const Conn::UseSSL ssl{true};
Conn c( ssl, Conn::UseSSL{true}, Conn::User{"alice"}, Conn::Pass{"$3cr37"} ); // ERROR: ssl is duplicated
==== code ====
full code with the examples can be downloaded here: {{:blog:2012:12:28:param_passing-rvalue-macros.cpp|parameter passing with rvalue references source}}. all the examples (including test code and proof-of-concept code can be obtained from here: {{:blog:2012:12:28:param_passing.tar.bz2|full sources and PoCs}}.
enjoy! :)