Implementing Default Parameters That Depend on Other Parameters in C++
C++ supports default parameters, but with some constraints.
We’ve seen that default arguments had to be positioned at the end of a function’s parameters, and also that default parameters are interdependent: indeed, to provide a non-default value to one of them, you have to also pass a value to those that come before it. We’ve seen how we could work around those constrains with Defaulted
.
But C++ default parameters also have another constraint: their default value can’t depend on other parameters. Let’s see how to improve Defaulted
to work around this constraint too.
This article is part of the series on default parameters:
- Default parameters in C++: the facts (including the secret ones)
- Should I overload or use default parameters?
- Default Parameters With Default Template Type Parameters
Defaulted
: a helper to work around default parameters constraints- Implementing Default Parameters That Depend on Other Parameters in C++
- How default parameters can help integrate mocks
EDIT: What follows consists in enriching Defaulted
so that it can take a function, rather than a value. Quite a few readers were kind enough to provide feedback on the technique that follows. It is too complicated: using a set of overloads instead reaches a better trade-off. Focused on trying to fit that feature into Defaulted
, I failed to see the bigger picture, where the simplest solution was to use something that has always been there in C++! Many thanks to all the people that took the time to express their feedback.
You can therefore consider this article deprecated.
Dependent default parameters?
Consider a function that takes several parameters:
void f(double x, double y, double z) { //... }
And say that in general, we would like one of them to be deduced from one or more of the other parameters. So for instance, we’d like to express the following, except this is not legal C++:
void f(double x, double y, double z = x + y) // imaginary C++ { //... }
One reason why this is not in the mindset of C++ is that C++ lets the compiler evaluate the arguments passed to the function in any order. So x
or y
could be evaluated after z
.
But, haven’t you ever needed this kind of behaviour? I feel this use case comes up every so often.
It would be nice to call f
without passing the last parameter in the general case:
f(x, y);
because the compiler can figure it out on its own with the default operation we provided. And only in some specific cases, we’d call f
with three parameters.
But we can’t do that in C++. So let’s try to work around that constraint, and implement this useful feature.
Making Defaulted
accept input values
The following is an attempt to work around the above constraint, and it is experimental. I’d love to hear your opinion on it.
Defaulted
already has a DefaultedF
variant, that accepts a function wrapped into a template type, function that takes no parameter and returns the default value:
struct GetDefaultAmount{ static double get(){ return 45.6; } }; void f(double x, double y, DefaultedF<double, GetDefaultAmount> z) { std::cout << "x = " << x << '\n' << "y = " << y << '\n' << "z = " << z.get_or_default() << '\n'; }
The above code can be called with:
f(1.2, 3.4, defaultValue);
and outputs:
x = 1.2
y = 3.4
z = 45.6
A default value that takes inputs
To make the default value depend on other parameters, we could let the default function accept values, that would be passed in when requesting the value from DefaultedF
:
struct GetDefaultAmount{ static double get(double x, double y){ return x + y; } }; void f(double x, double y, DefaultedF<double, GetDefaultAmount> z) { std::cout << "x = " << x << '\n' << "y = " << y << '\n' << "z = " << z.get_or_default(x, y) << '\n'; }
We would still call it with the same expression:
f(1.2, 3.4, defaultValue);
And we’d like to get the following output:
x = 1.2
y = 3.4
z = 4.6
How can we change the implementation of DefaultedF
to support this use case?
Implementation
Here is the implementation of DefaultedF
where we had left it:
template<typename T, typename GetDefaultValue> class DefaultedF { public: DefaultedF(T const& value) : value_(value){} DefaultedF(DefaultValue) : value_(GetValue::get()) {} T const& get_or_default() const { return value_; } T & get_or_default() { return value_; } private: T value_; };
The constructor takes in a value (or the information that this value should be default), and stores either a copy of the input value (it also deals with the case where T
is a reference but that’s outside of the scope of this article), or whatever the function in GetDefaultValue
returns. In both cases, the value to be used inside the function can be computed as soon as DefaultedF
is constructed.
This no longer holds true with our new requirement: if the call site actually passes in a value, DefaultedF
still knows its final value when it is constructed. But if the call site passes defaultValue
, then DefaultedF
will only know its final value when we pass in the x
and y
to the get_or_default
method.
So we need to hold a value that could either be set, or no set. Doesn’t that look like a job for optional?
Let’s therefore store an optional<T>
in the class instead of a T
. This optional is filled by the constructor taking an actual value, and the constructor taking a defaultValue
leaves it in its nullopt
state:
template<typename T, typename GetDefaultValue> class DefaultedF { public: DefaultedF(T const& t) : value_(t){} DefaultedF(DefaultValue) : value_(std::nullopt) {} // ... private: std::optional<T> value_; };
Now it’s the get_or_value()
methods that does the job of calling the function in GetDefaultValue
if the optional is empty:
template<typename... Args> T get_or_default(Args&&... args) { if (value_) { return *value_; } else { return GetDefaultValue::get(std::forward<Args>(args)...); } }
Note that we return a T
by value. I’m not happy about that, but it seems necessary to me since in the case where the optional is empty, we return whatever the function returns, which could be a temporary object. So we can’t return a reference to it.
Let’s try it out:
struct GetDefaultAmount{ static double get(double x, double y){ return x + y; } }; void f(double x, double y, DefaultedF<double, GetDefaultAmount> z) { std::cout << "x = " << x << '\n' << "y = " << y << '\n' << "z = " << z.get_or_default(x, y) << '\n'; }
With this call site:
f(1.2, 3.4, defaultValue);
outputs:
x = 1.2 y = 3.4 z = 4.6
as expected.
Have you ever encountered the need of having default values depending on other parameters? What do you think of the way that DefaultedF
uses to approach that question?
You’ll find all the code of the Defaulted
library in its Github repository.
Related articles:
- Default parameters in C++: the facts (including the secret ones)
- Should I overload or use default parameters?
- Default Parameters With Default Template Type Parameters
Defaulted
: a helper to work around default parameters constraints- How default parameters can help integrate mocks
Share this post!