Jonathan Boccara's blog

How to Design Function Parameters That Make Interfaces Easier to Use (2/3)

Published November 27, 2018 - 8 Comments

Daily C++

Let’s continue exploring how to design function parameters that help make both interfaces and their calling code more expressive.

If you missed on the previous episode of this topic, here is what this series of articles contains:

  • Part 1: interface-level parameters, one-parameter functions, const parameters,
  • Part 2: calling contexts, strong types, parameters order,
  • Part 3: packing parameters, processes, levels of abstraction.

Don’t tie a parameter to one calling context

Sometimes, we design a function to solve a particular problem. To illustrate this, let’s represent a bag that contains books. Both Bag and Book derive from the same interface Item that has a getVolume function. And here we need a function to determine what proportion of the bag space this given book takes up.

This function takes two parameters of type Item. Of course we don’t want this tied to Bag and Book in particular, so let’s write a function with more generic names and types, like item and containingItem:

double getRatio(Item const& item, Item const& containingItem)
{
    return item.getVolume() / containingItem.getVolume();
}

Here is how we would call the function:

double allotedSpaceInBag = getRatio(book, bag);

And then we encounter a new context: this time we have two editions of the same book, the old edition and the current edition. And we want to know how much in volume the new edition is compared to the old one. We need this to know this to determine how much more daunting this new edition looks like, compared to the old one (told you, I stripped off the original domain!).

Then we want to reuse our function, because it is the same formula we want to use:

double getRatio(Item const& item, Item const& containingItem);

double increasedFearOfReading = getRatio(book_v2, book_v1);

And all of a sudden, your interfaces that used to work stops making sense: why are we passing the book in Version 1 as a “containing” item?

This is the right time to think about what our function parameters are supposed to mean exactly. When we have only one context, it’s sometimes hard to see where the function stops and where the context starts. But with two (or more) different contexts, the function’s abstraction becomes clearer, as the various contexts draw a picture in negative of it.

Here is how to fix our interface:

double getRatio(Item const& item, Item const& referenceItem);

double allotedSpaceInBag = getRatio(book, bag);
double increasedFearOfReading = getRatio(book_v2, book_v1);

And then it makes sense again. The reason why this works is that we’ve given names that relate to the function itself (one item is the “reference” during the comparison), and not to one particular context. Said differently, we’ve given names that belong to the level of abstraction of the interface, and not in the higher level of abstraction of the context.

So to put that in practice, I encourage you to:

  • think hard about the level of abstraction of your interface when you give names to your funciton parameters,
  • as soon as you have more than one context that uses a function, put that extra knowledge into practice to refine your function parameters names so they become independent from the various contexts.

Use strong types to make calling your interfaces a no-brainer

Consider this line of code from the previous example:

double increasedFearOfReading = getRatio(book _v2, book_v1);

Are we sure that we passed the parameters in the right order? Maybe we’ve mixed up the parameters by mistake and just computed the decrease in fear of reading, and there is a bug in our calling code. Strong types help with that by using the type system to check that you pass the right parameters at the right places.

In short, strong typing consist in creating a new surrogate type that carries a more specific name such as ReferenceItem, and that wraps Item:

class ReferenceItem
{
public:
    explicit ReferenceItem(Item const& item) : item_(item) {}
    Item const& get() const { return item_; }
private:
    Item const& item_;
};

Here is how we can use it in our function’s interface:

double getRatio(Item const& item, ReferenceItem const& referenceItem)
{
    return item.getVolume() / referenceItem.get().getVolume();
}

And then the call site loses all ambiguity:

getRatio(book_v2, ReferenceItem(book_v1)) // now we know that v1 is the reference

In fact there is more to strong types than that. Much more. To learn about them, check out the series on strong types of Fluent C++ that covers many aspects of that important topic.

In which order should we define function parameters?

There are various conventions about the order of the parameters of a function. Here we review a couple of possible conventions, but beyond choosing a specific one, the most important thing is to have one convention, and follow it consistently.

Indeed, when your team puts a convention in place, then the order of the arguments passed to a function sends you a signal, even in a piece of code that you don’t know. Your convention can be following one of these, but can also be a combination that associaties each convention to a type of case.

in – inout – out

This is a fairly common convention, and not only in C++:

  • put the parameters that the function uses as inputs first,
  • then put the parameters that the function uses both as input and as outputs (so the function modifies them),
  • finally, put the parameters that the function uses as outputs (the function outputs its results in them) last.

There is one thing that this convention doesn’t take into account: outputs should be in the return type of the function, not in the parameters, which makes for clearer code.

However in some pathological cases you can’t return the output of a function. For instance a class that is copyable via a copy function and not by its copy constructor. It exists, and sometimes you don’t have the time to refactor it as a prerequisite of your main development. In this case, you are forced to pass outputs as arguments, and this convention make sense.

Main parameter first

Consider a succession of function that progressively build something, like a car for example. The last one of those functions paints the car in a certain color:

void paint(Car& car, Color color);

Then the important parameter here is the Car and we put it first. It is different from the previous convention since car it is an in-and-out parameter, so the previous convention would want it after color because color it is an input.

Note however that in-and-out parameters should not be the default case for functions. The clearest type of function call is when it only takes inputs as parameters (and return outputs via the return type). In this case, the “main” parameter is a blurrier choice.

Explicit parmeter roles

When a function takes several parameters that share similarities, say 5 collections for example, it can be useful for the interface to be very explicit about which are inputs and which are outputs. You can achive this with comments:

void myFunction(
/* IN */ 
std::vector<Foo> const& foos,
std::vector<Bar> const& bars,
/* IN-OUT */
std::vector<Baz>& bazs,
std::vector<Foo>& otherFoos,
std::vector<Mice>& earthBuilders);

You could achieve this with macros too:

#define IN
#define INOUT

void myFunction(
std::vector<Foo> const& foos IN,
std::vector<Bar> const& bars IN,
std::vector<Baz>& bazs INOUT,
std::vector<Foo>& otherFoos INOUT,
std::vector<Mice>& earthBuilders INOUT);

But with all the drawbacks of macros, I don’t think they’re worth it here.

Here is an suggestion of convention that combines all of the above:

  • for functions that have an obvious main parameter, put this one first,
  • for functions taking outputs in parameters, do in – inout – out,
  • for functions that take several parameters that look alike, resort to comments to be more explicit.

Whichever convention you choose, the important thing is to agree on one, and to share it between the developers working on the same codeline so that it is consistent in this regard.

Stay tuned for the third episode of this series on function parameters, where we focus on packing parameters, processes and levels of abstraction!

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

Comments are closed