Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named Arguments in C #11936

Open
guevara opened this issue Dec 17, 2024 · 0 comments
Open

Named Arguments in C #11936

guevara opened this issue Dec 17, 2024 · 0 comments

Comments

@guevara
Copy link
Owner

guevara commented Dec 17, 2024

Named Arguments in C++



https://ift.tt/0olJTta



Jonathan Boccara


Ah, named arguments!

If the term “feature envy” wasn’t already used to designate a refactoring, we would have employed it to talk about named arguments: it’s a feature that languages that don’t have it envy to the languages that do.

Named arguments consist in specifying at call site the parameter names of the arguments passed. For instance, consider this function:

void displayCoolName(std::string const& firstName, std::string const& lastName)
{
    std::cout << "My name is " << lastName << ", " << firstName << ' ' << lastName << '.';
}

The call site of that function looks like this:

displayCoolName("James", "Bond");

(hey, want to try it out with your own name to see how cool your name sounds?)

With named arguments, the call site would rather look like that:

displayCoolName(firstName = "James", lastName = "Bond");

It has the advantage of being more explicit so that you don’t mix up the order of the parameters. Also, a reader of the call site doesn’t have to go check the function’s prototype to understand the meaning of the function’s parameters.

Some languages have this. Objective-C has it, Python has something not far, hell even C99 has something resembling it for initiating structures.

And us in C++? We stand here, envying the feature.

Envy no more, here is a technique to implement named arguments in C++.

I will present this in 3 steps:

  • step 1: strong types to differentiate parameters,
  • step 2: a trick to get the right syntax,
  • step 3: going further: passing the parameters in any order.

I want to thank Reddit user /u/matthieum from which I got largely inspired when he commented on the Reddit thread of Strong types for strong interfaces.

Step 1: Strong types to differentiate parameters

If you are a regular reader of Fluent C++, you probably have already encountered strong types.

Strong types consist in replacing a type with another one that adds meaning through its name. In our above example we could create two strong types, FirstName and LastName that both wrap a std::string to pin a specific meaning (like representing a first name, or a last name) over it.

For this we will use the NamedType library, of which you can get a overview on its GitHub page or in Strong types for strong interfaces, if you’re not familiar with it.

using FirstName = NamedType<std::string, struct FirstNameTag>;
using LastName = NamedType<std::string, struct LastNameTag>;

These are two different types. They both wrap a std::string that they expose through their .get() method.

Let’s replace the naked std::strings in our displayCoolName function with those strong types:

void displayCoolName(FirstName const& firstName, LastName const& lastName)
{
    std::cout << "My name is " << lastName.get() << ", " << firstName.get() << ' ' << lastName.get() << '.';
}

Now here is what a call site looks like:

displayCoolName(FirstName("James"), LastName("Bond"));

That can play the role of named arguments, and it would already be reasonable to stop here.

But let’s wrap around the C++ syntax to get to those oh-so-enviable named arguments, with the equal sign and all.

Step 2: A trick to get the right syntax

We would like to be able to write a call site like this one:

displayCoolName(firstName = "James", lastName = "Bond");

Let’s reason about this syntax: we need the first argument to be of type FirstName. So we need to define an object firstName that has an operator= that takes a std::string (or something convertible to it) and that returns a FirstName.

Let’s implement the type of this object firstName. We call this type argument. Since argument must know FirstName, which is a template class, I think the most convenient is to implement argument as a nested class inside the class FirstName.

FirstName is an alias of NamedType, so let’s add argument inside NamedType:

template< /* the template args of NamedType */ >
class NamedType
{
public:
    // ...
struct argument
{
    template&lt;typename UnderlyingType&gt;
    NamedType operator=(UnderlyingType&amp;&amp; value) const
    {
        return NamedType(std::forward&lt;UnderlyingType&gt;(value));
    }
};

};

We can now create the firstName and lastName helpers to accompany our function:

static const FirstName::argument firstName;
static const LastName::argument lastName;

void displayCoolName(FirstName const& theFirstName, LastName const& theLastName)
{
std::cout << "My name is " << theLastName.get() << ", " << theFirstName.get() << ' ' << theLastName.get() << '.';
}

And now the call site of displayCoolName looks at last like this:

displayCoolName(firstName = "James", lastName = "Bond");

Yay, named arguments!

The NamedType library now has this feature available.

As a side note, since the firstName and lastName helpers are not supposed to be passed to a function, let’s delete the default-generated move and copy methods:

struct argument
{
    template<typename UnderlyingType>
    NamedType operator=(UnderlyingType&& value) const
    {
        return NamedType(std::forward<UnderlyingType>(value));
    }
    argument() = default;
    argument(argument const&) = delete;
    argument(argument&&) = delete;
    argument& operator=(argument const&) = delete;
    argument& operator=(argument&&) = delete;
};

Step 3: Going further: passing the parameters in any order

Since we indicate which argument corresponds to what parameter, do we really need a fixed order of arguments?

Indeed, it would be nice if any given call site had the choice to write either this:

displayCoolName(firstName = "James", lastName = "Bond");

or that:

displayCoolName(lastName = "Bond", firstName = "James");

and that it would have the same effect.

We’re going to see a way to implement this. However, I don’t think it is production-ready because of some readability drawbacks that we will see.

So from his point on we’re threading into the exploratory, and of course your feedback will be welcome.

Since we don’t know the types of the first and second parameter (either one could be FirstName or LastName), we are going to turn our function into a template function:

template<typename Arg0, typename Arg1>
void displayCoolName(Arg0&& arg0, Arg1&& arg1)
{
    ...

Now we need to retrieve a FirstName and a LastName from those arguments.

Picking an object of a certain type amongst several objects of different types sounds familiar: we can use std::get on a std::tuple like when we used strong types to return multiple values.

But we don’t have a std::tuple, we only have function arguments. Fortunately, there is nothing easier than packing function arguments into a std::tuple, thanks to the std::make_tuple function. The resulting code to pick a type looks like this:

template<typename TypeToPick, typename... Types>
TypeToPick pick(Types&&... args)
{
    return std::get<TypeToPick>(std::make_tuple(std::forward<Types>(args)...));
}

Let’s use this to retrieve our FirstName and LastName from the arguments:

template<typename Arg0, typename Arg1>
void displayCoolName(Arg0&& arg0, Arg1&& arg1)
{
    auto theFirstName = pick<FirstName>(arg0, arg1);
    auto theLastName = pick<LastName>(arg0, arg1);
    std::cout << "My name is " << theLastName.get() << ", " << theFirstName.get() << ' ' << theLastName.get() << '.' << '\n';
}

Now we can call either:

displayCoolName(firstName = "James", lastName = "Bond");

or:

displayCoolName(lastName = "Bond", firstName = "James");

And in both cases we get:

My name is Bond, James Bond.

C++ named arguments

One of the drawbacks that I see with this latest technique is that it converts our function into a template. So it needs to go to a header file (unless we do explicit instantiation of all the permutations of the arguments).

To mitigate this, we could extract a thin layer that picks the arguments and forwards them the the function as it was before:

// .hpp file

void displayCoolNameImpl(FirstName const& theFirstName, LastName const& theLastName);

template<typename Arg0, typename Arg1>
void displayCoolName(Arg0&& arg0, Arg1&& arg1)
{
displayCoolNameImpl(pick<FirstName>(arg0, arg1), pick<LastName>(arg0, arg1));
}

// .cpp file

void displayCoolNameImpl(FirstName const& theFirstName, LastName const& theLastName)
{
std::cout << "My name is " << theLastName.get() << ", " << theFirstName.get() << ' ' << theLastName.get() << '.' << '\n';
}

Another drawback is that the names of the parameters in the prototype lose all their meaning (“Arg0″…).

If you see other drawbacks, or if you see how to improve this technique to be able to pass function argument in any order, share it in a comment!

Don't want to miss out ? Follow: &nbsp&nbsp

linkedin

Share this post!

Facebooklinkedin







via Fluent C++

December 17, 2024 at 03:54PM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant