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:
- Requires that all optional parameters be of different types. I believe this is tolerable especially in light of type safe enums.
- Multiple optional parameters with same type will not cause compile errors. This could be mitigated with C++14
std::get<T>(tuple)
. - Modern IDEs will not be able to help with the optional parameter names.
1 Answer 1
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;
}
-
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\$Thomas Zwaagstra– Thomas Zwaagstra2018年02月15日 22:11:22 +00:00Commented Feb 15, 2018 at 22:11
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\$