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. 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<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...); }
  E default_;
// ...
class Conn
  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...) )
  { }
  // ...
  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:

  1. it does a lot of unneeded copies.
  2. it always created default value, that sometimes is overwritten.
  3. 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<typename E>
struct extract
  /** @brief extracts type E from the Args argument list, default value or compilation error. */
  static E from(Args&&...args)
    ensureUnique( std::forward<Args&&>(args)... );      // sanity check
    return fromImpl( std::forward<Args&&>(args)... );   // actuall implementation
  /** @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)
  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>
  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>
  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.

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<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{};}       )

example class declaration

having this done, usage is really simple:

class Conn
  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
    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) )
  { }
  // ...
  BNP_PARAM_TYPE(Host)       host_;
  BNP_PARAM_TYPE(User)       user_;
  BNP_PARAM_TYPE(Pass)       pass_;
  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! :)

blog/2012/12/28/1.txt · Last modified: 2021/06/15 20:09 by
Back to top
Valid CSS Driven by DokuWiki Recent changes RSS feed Valid XHTML 1.0