1
\$\begingroup\$

Intent

Increase code usability/readability of functions with many default parameters while maintaining as much compile time safety as possible.

Motivation

It is very common to create functions that have many default parameters that extend and customise their side-effects. A problem arises in function usability/readability when the parameter you want to overload is far down in the list but you want all the preceding parameters at their default values. This method also allows for optional parameters to be used as parameters in generating other optional parameters of the function (which cannot be done with default parameters).

Solution

template <typename... T>
void function_with_optional_params(int required_param, T... args) {
  std::tuple<T...> tuple(std::forward<T>(args)...);
  static_assert(std::tuple_size<std::tuple<T...>>::value <= 4, "To many arguments");
  static_assert(xtd::all_true<
              std::is_same<T, int>{} ||
              std::is_same<T, std::string>{} ||
              std::is_same<T, double>{} ||
              std::is_same<T, float>{}
              ...>{}, "Unexpected argument types");

  int first = xtd::get_first_default_func<int>(tuple, [](){ return -1; });
  std::string second = xtd::get_first_default_value<std::string>(tuple, "-2");
  double third = xtd::get_first_default_value<double>(tuple, -3.0);
  float forth = xtd::get_first_default_func<float>(tuple, [](){ return -4.0; });

  std::cout << required_param << "," << first << "," << second << "," << third << "," << forth << std::endl;
}

Helper Functions

namespace xtd {
  namespace detail {
    template <class T, std::size_t N, class... Args>
    struct get_number_of_element_from_tuple_by_type_impl {
      static constexpr auto value = N;
    };

    template <class T, std::size_t N, class... Args>
    struct get_number_of_element_from_tuple_by_type_impl<T, N, T, Args...> {
       static constexpr auto value = N;
     };

     template <class T, std::size_t N, class U, class... Args>
     struct get_number_of_element_from_tuple_by_type_impl<T, N, U, Args...> {
       static constexpr auto value = get_number_of_element_from_tuple_by_type_impl<T, N + 1, Args...>::value;
     };

   } // namespace detail

   template <class T, class... Args>
   T get_first(const std::tuple<Args...>& t) {
     return std::get<detail::get_number_of_element_from_tuple_by_type_impl<T, 0, Args...>::value>(t);
   }

   template <class T, class... Args>
   typename std::enable_if<detail::get_number_of_element_from_tuple_by_type_impl<T, 0, Args...>::value < std::tuple_size<std::tuple<Args...>>::value, T>::type
   get_first_default_value(const std::tuple<Args...>& t, T d) {
     return get_first<T>(t);
   }
   template <class T, class... Args>
   typename std::enable_if<detail::get_number_of_element_from_tuple_by_type_impl<T, 0, Args...>::value == std::tuple_size<std::tuple<Args...>>::value, T>::type
   get_first_default_value(const std::tuple<Args...>& t, T d) {
     return d;
   }

   template <class T, class D, class... Args>
   typename std::enable_if<detail::get_number_of_element_from_tuple_by_type_impl<T, 0, Args...>::value < std::tuple_size<std::tuple<Args...>>::value, T>::type
   get_first_default_func(const std::tuple<Args...>& t, D d) {
     return get_first<T>(t);
   }
   template <class T, class D, class... Args>
   typename std::enable_if<detail::get_number_of_element_from_tuple_by_type_impl<T, 0, Args...>::value == std::tuple_size<std::tuple<Args...>>::value, T>::type
   get_first_default_func(const std::tuple<Args...>& t, D d) {
     return d();
   }

   template <bool...> struct bool_pack;

   template <bool... v>
   using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;
 }

Example Usage

// HTTP Request with optional callbacks and headers
void make_explicit_request(
  client client,
  method method,
  const std::string& url,
  callback_func on_callback,
  headers custom_headers = headers(),
  request_header_sender::send_func on_send = nullptr,
  resolve_error_func on_resolve_error = nullptr,
  connect_error_func on_connect_error = nullptr);

template <typename... T>
void make_request(
  client client,
  method method,
  const std::string& url,
  callback_func on_callback,
  T... args) {
  std::tuple<T...> tuple(std::forward<T>(args)...);
  static_assert(std::tuple_size<std::tuple<T...>>::value <= 4, "To many arguments");
  static_assert(xtd::all_true<
              std::is_same<T, headers>{} ||
              std::is_same<T, request_header_sender::send_func>{} ||
              std::is_same<T, resolve_error_func>{} ||
              std::is_same<T, connect_error_func>{}
              ...>{}, "Unexpected argument types");

  headers custom_headers = xtd::get_first_default_func<headers>(tuple, [](){ return headers(); });
  request_header_sender::send_func on_send = xtd::get_first_default_value<request_header_sender::send_func>(tuple, nullptr);
  resolve_error_func on_resolve_error = xtd::get_first_default_value<resolve_error_func>(tuple, nullptr);
  connect_error_func on_connect_error = xtd::get_first_default_value<connect_error_func>(tuple, nullptr);
 
  make_explicit_request(client, method, url, on_callback, custom_headers, on_send, on_resolve_error, on_connect_error);
}
// Open a file for async read and/or write
void open(
  loop loop,
  const std::string& path,
  int flags, int mode,
  open_func on_open,
  error_func on_open_error = nullptr,
  complete_func on_close = nullptr,
  error_func on_close_error = nullptr);

Known Issues

The code works as intended everywhere I use it, however these points need to be kept in mind:

  1. Requires that all optional parameters be of different types. I believe this is tolerable especially in light of type safe enums.
  2. Multiple optional parameters with same type will not cause compile errors. This could be mitigated with C++14 std::get<T>(tuple).
  3. Modern IDEs will not be able to help with the optional parameter names.
\$\endgroup\$
8
  • 7
    \$\begingroup\$ It is very common to create functions that have many default parameters that extend and customise their side-effects. Disagree. This is very uncommon and usually a sign of bad interface design. \$\endgroup\$ Nov 21, 2016 at 19:32
  • \$\begingroup\$ @LokiAstari Then what would a more proper interface look like for handling async completion and/or error callbacks such as in the example interfaces provided? \$\endgroup\$ Nov 21, 2016 at 22:39
  • \$\begingroup\$ You could pass an object with fields for all the special cases. Then have appropriate constructors build the object correctly depending on what parameters are passed. But that is just the first thought of the top of my head. This is C++ you could do it a million different ways. I just disagree this is (1) Common issue. (2) Have lots of ways to customize side effects. \$\endgroup\$ Nov 22, 2016 at 18:59
  • \$\begingroup\$ Another way: Have one object that controls all the side affect code. Each different type of side affect in a different virtual functions. \$\endgroup\$ Nov 22, 2016 at 19:02
  • \$\begingroup\$ PS. Given the code above I am still unclear how this works. You need some better examples with more explanation. Currently I can review this as I don't understand how it is supposed to be used or work. \$\endgroup\$ Nov 22, 2016 at 19:04

1 Answer 1

2
\$\begingroup\$

Chainable configuration object

Necro! Rather than relying on many default parameters, consider building up a configuration object. If you use setters for your options, you can build a nice chainable interface like this:

result = inventory.find(id).color(red).size(large).get()

Benefits

  • Allows specifying arguments in any order or not at all
  • Allows for required arguments
  • Compile-time safe
  • IDE can suggest parameters
  • A C++ novice can understand it

Example

Here's a minimal-ish example that adds 3 numbers together, with default values for the second and third numbers

#include <iostream>

// Forward declaration
class Config;

// The class that performs the actual work
class MyThing
{
  friend class Config;
  public:
  // Wrapping function
  Config doWork(int required1);
  private:
  // Real work function
  int work(int required1, int option1, int option2);
};

class Config
{
  public:
  Config(MyThing& thing, int required1);
  int execute();

  // Options
  Config& withOption1(int option);
  Config& withOption2(int option);

  private:
  MyThing& thing; // Reference to the object which will do the work

  // Required & optional parameters with defaults
  int required1;
  int option1 = 5;
  int option2 = 7;
};

Config MyThing::doWork(int required1)
{
  return Config(*this, required1);
}

int MyThing::work(int required1, int option1, int option2) 
{
  return required1 + option1 + option2;
}

Config::Config(MyThing& thing, int required1) : thing(thing), required1(required1)
{

}

int Config::execute()
{
  return thing.work(required1, option1, option2); 
}

Config& Config::withOption1(int option)
{
  option1 = option;
  return *this;
}

Config& Config::withOption2(int option)
{
  option2 = option;
  return * this;
}

int main(int argc, char** argv)
{
  MyThing thing;
  int result = thing.doWork(10).withOption2(3).execute(); // Option 1 is defaulted!
  std::cout << "The result is " << result << std::endl;
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This is probably the wrong type of answer for this site, but consider critique being the relative simplicity of this kind of solution as compared to the proposed solution. \$\endgroup\$ Feb 15, 2018 at 22:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.