-1

What are the consequences (pros and cons) of using a single structure as the argument, instead of a list of parameters. Some of the advantages I see are:

  1. create multiple default parameters.
  2. Need not reply on the order of the parameters.
  3. In case of a refactoring, need not change the signature of the method.

Some disadvantages I expect are:

  • May be difficult to handle parameters with "&".
  • May not make sense to use "function overloading" and we may have to put all the possible parameters in a single structure and all the combination of the parameters may not make sense all the time!

What are the other pros and cons of this approach?

7

OK, so what you're talking about is a function of the form:

struct func_params
{
  int param1;
  float param2;
  ...
};

void func(func_params params) {...}

I will assume that the hypothetical func_params is legitimately useful only for calling func. That the values func_params stores have no logical connection beyond the fact that they are what func needs to do its job.

Pre-C++11, users would generally call your function using code like this:

func_params params = {...};
func(params);

Given the above assumption, this creates a stack variable whose utility does not extend beyond the call to func.

But since you tagged your question C++11, users can call your function with func({...}) instead through list initialization.

However in both cases, order does matter. The order of the values in func_params maps directly to the order of values in the list initializer {...}.

Of course, C++11's aggregate rules don't allow default member initializers in aggregates. C++14 does.

Now yes, a user could do this:

func_params params;
params.param1 = ...;
params.param2 = ...;
...
func(params);

But seriously, who wants to write that? And who wants to read it? As such, whenever possible, users will avoid the added verbosity of the long form, in favor of the short form. And with the short form, default values in the middle of a sequence don't matter.

C++20's designated initializers will save you from the default value issue, but not from order dependency. Yes, they'll allow you to do func({.param1 = ..., .param2 = ..., ...}), but the C++20 feature doesn't allow you to reorder them. You can omit items (they'll be value initialized or initialized by default member initializers), but you are not permitted to reorder any aggregate members you specify.

So "Need not reply on the order of the parameters." is incorrect. Or at least, only correct for the long form that people will assiduously avoid.

But there's the ABI problem. func's ABI is ultimately tied to func_params. If this is in an interface, then changes to func_params will affect everyone who uses it. So while you are not directly changing the signature, you are effectively doing so.

And this would be just as true if func took func_params as a const& rather than as a value. Imagine what would happen if you gave a previously non-default "parameter" a default value. Every user who called your function would have to be recompiled, since it is the job of the caller to fill in default values.

To me, doing this is only valuable in C++20 land where you have access to designated initializers. Why? Because it allows you to have the equivalent of named parameters. So if a function has a large number of different parameters, you can see names for these values at the call site. This makes it easier for people to see what the meaning of each parameter is.

Oh sure, you can do it the long-form way, where you declare a variable and set the values you want specifically. But again, who wants to write or read that?

And even then, this is only useful for functions with large numbers of parameters. It is usually best to avoid such scenarios, to reduce function parameter counts by creating meaningful aggregations of them.

Remember that assumption I made at the beginning? More often than not, functions that take a lot of parameters will have some meaningful connections between certain groups of parameters. A function that tests whether a 2D point is in a rectangle could be written as:

bool pt_in_rect(int rect_ul_x, int rect_ul_y, int rect_br_x, int rect_br_y, int test_x, int test_y);

But isn't this more reasonable?

struct point2d { int x; int y; };
bool pt_in_rect(point2d rect_ul, point2d rect_br, point2d test);

And isn't this even better?

struct rect { point2d top_left; point2d bottom_right};
bool pt_in_rect(rect box, point2d test);

This is not equivalent to what you're talking about. Why? Because rect and point2d have semantic meaning. They're not arbitrary aggregations of values; you can imagine dozens or hundreds of functions that operate on rectangles or 2d points. These aggregations impart meaning on their component values.

You don't build these structs just because pt_in_rect exists; you build them because they make sense, and pt_in_rect's interface becomes far more digestible because of it.

Now of course, there are times when a function genuinely needs a lot of genuinely disparate values, with no real meaning between collections of them. But these cases are fairly uncommon. In most cases, you can reduce parameter counts by providing reasonable abstractions on data types, like the above.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.