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.
C++ lacks support for named parameters, though (as usual) it can be overcome. 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.
recently i've came to the conclusion that the same can be done suing variadic templates and perfect forwarding.
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<typename E> struct extract { template<typename T> explicit extract(T t): default_{t} { } E from(void) { return default_; } template<typename ...Tail> E from(E e, Tail...) { return e; } template<typename Head, typename ...Tail> 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<typename ...Args> explicit Conn(Args...a): host_( extract<Host>("my.db.org").from(a...).value_ ), user_( extract<User>("john").from(a...).value_ ), pass_( extract<Pass>("doe1940").from(a...).value_ ), useSSL_( extract<UseSSL>(false).from(a...).value_ ), retCnt_( extract<RetryCount>(10u).from(a...).value_ ), my_( extract<MyClass>(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:
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.
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<typename E> struct extract { /** @brief extracts type E from the Args argument list, default value or compilation error. */ template<typename...Args> static E from(Args&&...args) { ensureUnique( std::forward<Args&&>(args)... ); // sanity check return fromImpl( std::forward<Args&&>(args)... ); // actuall implementation }; private: /** @brief short for remove_cv and decay on a given type. */ template<typename T> struct Raw { typedef typename remove_cv<typename decay<T>::type>::type type; }; // // counts number of occurences of given type C in the argument list. // template<typename C, typename Head, typename...Tail> static constexpr size_t count(void) { return (is_same< typename Raw<C>::type, typename Raw<Head>::type >::value?1:0) + count<C,Tail...>(); } template<typename C> static constexpr size_t count(void) { return 0; } // // extraction itself // /** @brief template used for signalling missing parameter of a given type. */ template<typename T> constexpr bool missingParameter(void) { return false; } /** @brief causes compilation error when required parameter is not supplied. */ static E fromImpl(NoDefaultValue&&) { static_assert( missingParameter<E>(), "missing paramter with no default value: " ); } /** @brief created default agrument, using user-provided functor. */ template<typename F> static E fromImpl(F&& f) { return f(); } /** @brief extractor of a given paramter, when it has been found. */ template<typename Head, typename ...Tail> static typename enable_if< is_same< typename Raw<Head>::type, E >::value, E >::type fromImpl(Head&& e, Tail&&...) { return std::forward<Head&&>(e); } /** @brief general search - no proper value found yet. */ template<typename Head, typename ...Tail> static typename enable_if< !is_same< typename Raw<Head>::type, E >::value, E >::type fromImpl(Head&&, Tail&&...t) { return fromImpl(std::forward<Tail&&>(t)...); } // // wrapper call that ensures all arguments are unique. // template<typename Head, typename...Tail> static void ensureUnique(Head&& head, Tail&&...tail) { static_assert( count<Head,Tail...>() <= 0, "argument is repeated" ); // check current one ensureUnique( std::forward<Tail&&>(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.
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<typename ...Args> 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<type>::from( std::forward<Args&&>(a)..., NoDefaultValue{} ) /** @brief the same as BNP_EXTRACT but with the explicitly provided default value. */ #define BNP_EXTRACT_D(type, defVal) extract<type>::from( std::forward<Args&&>(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<type>::from( std::forward<Args&&>(a)..., [](){return type{};} )
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_; };
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
full code with the examples can be downloaded here: parameter passing with rvalue references source. all the examples (including test code and proof-of-concept code can be obtained from here: full sources and PoCs.
enjoy! :)