Why aren't named parameters used more often?

Fredrik picture Fredrik · Apr 6, 2012 · Viewed 16.2k times · Source

I have designed a parameter class which allows me to write code like this:

//define parameter
typedef basic_config_param<std::string> name;

void test(config_param param) {

  if(param.has<name>()) { //by name
    cout << "Your name is: " << param.get<name>() << endl;
  }

  unsigned long & n = param<ref<unsigned long> >(); //by type
  if(param.get<value<bool> >(true)) { //return true if not found
    ++n;
  }
}


unsigned long num = 0;
test(( name("Special :-)"), ref<unsigned long>(num) )); //easy to add a number parameter
cout << "Number is: " << num; //prints 1

The performance of the class is pretty fast: everything is just a reference on the stack. And to save all the information I use an internal buffer of up to 5 arguments before it goes to heap allocation to decrease the size of every single object, but this can be easily changed.

Why isn't this syntax used more often, overloading operator,() to implement named parameters? Is it because of the potential performance penalty?

One other way is to use the named idiom:

object.name("my name").ref(num); //every object method returns a reference to itself, allow object chaining.

But, for me, overloading operator,() looks much more "modern" C++, as long you don't forget to uses double parentheses. The performance does not suffer much either, even if it is slower than a normal function, so is it negligible in most cases.

I am probably not the first one to come up with a solution like this, but why isn't it more common? I have never seen anything like the syntax above (my example) before I wrote a class which accepts it, but for me looks it perfect.

Answer

SigTerm picture SigTerm · Apr 6, 2012

My question is why this syntax is not used more, overloading operator,() to implement named parameters.

Because it is counter-intuitive, non-human-readable, and arguably a bad programming practice. Unless you want to sabotage the codebase, avoid doing that.

test(( name("Special :-)"), ref<unsigned long>(num) ));

Let's say I see this code fragment for the first time. My thought process goes like this:

  1. At a first glance it looks like an example of "the most vexing parse" because you use double-parentheses. So I assume that test is a variable, and have to wonder if you forgot to write variable's type. Then it occurs to me that this thing actually compiles. After that I have to wonder if this is an instance of an immediately destroyed class of type test and you use lowercase names for all class types.
  2. Then I discover it is actually a function call. Great.
  3. The code fragment now looks like a function call with two arguments.
  4. Now it becomes obvious to me that this can't be a function call with two arguments, because you used double parentheses.
  5. So, NOW I have to figure what the heck is going on within ().
  6. I remember that there is a comma operator (which I haven't ever seen in real C++ code during the last 5 years) which discards the previous argument. SO NOW I have to wonder what is that useful side effect of name(), and what the name() is - a function call or a type (because you don't use uppercase/lowercase letters to distinguish between class/function (i.e. Test is a class, but test is a function), and you don't have C prefixes).
  7. After looking up name in the source code, I discover that it is class. And that it overloads the , operator, so it actually doesn't discard the first argument anymore.

See how much time is wasted here? Frankly, writing something like that can get you into trouble, because you use language features to make your code look like something that is different from what your code actually does (you make a function call with one argument look like it has two arguments or that it is a variadic function). Which is a bad programming practice that is roughly equivalent to overloading operator+ to perform substractions instead of additions.

Now, let's consider a QString example.

 QString status = QString("Processing file %1 of %2: %3").arg(i).arg(total).arg(fileName);

Let's say I see it for the first time in my life. That's how my thought process goes:

  1. There is a variable status of type QString.
  2. It is initialized from a temporary variable of type QString().
  3. ... after QString::arg method is called. (I know it is a method).
  4. I look up .arg in the documentation to see what it does, and discover that it replaces %1-style entries and returns QString&. So the chain of .arg() calls instantly makes sense. Please note that something like QString::arg can be templated, and you'll be able to call it for different argument types without manually specifying the type of argument in <>.
  5. That code fragment now makes sense, so I move on to another fragment.

looks very more "modern" C++

"New and shiny" sometimes means "buggy and broken" (slackware linux was built on a somewhat similar idea). It is irrelevant if your code looks modern. It should be human-readable, it should do what it is intended to do, and you should waste the minimum possible amount of time in writing it. I.e. you should (personal recommendation) aim to "implement a maximum amount of functionality in a minimum amount of time at a minimum cost (includes maintenance)", but receive the maximum reward for doing it. Also it makes sense to follow KISS principle.

Your "modern" syntax does not reduce development cost, does not reduce development time, and increases maintenance cost (counter-intuitive). As a result, this syntax should be avoided.