Jonathan Boccara's blog

Implementing Default Parameters That Depend on Other Parameters in C++

Published August 17, 2018 - 6 Comments

Dependent default parameters

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:

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:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed