Functional statements in C++

Learning new programming languages helps in acquiring new perspectives and idioms.

Before learning Scala I was perfectly fine with C++ imperative statements, if you want to retrieve a value out of them just change a variable in the outer scope.

The example below is a bit artificial, but I’m sure you recognize the pattern you already used countless times in your imperative code.

bool f( int x ) {
  bool ok;
  if( x == 1 ) {
    printf("yeah\n");
    ok=true;
  }
  else {
    printf("ouch\n");
    ok = false;
  }
  return ok;
}

Although fine it had one annoying quirk – you need a dedicated outer scope variable (ok in the example). You could leave that variable uninitialized since you will initialize rightly thereafter, but leaving an uninitialized variable is a bad habit, reported as a warning by the compiler and forbidden by many coding guidelines.

But the initialization is just something dummy to silence the warning and doesn’t prevent any bug. I mean, if I forget to assign a value in one of the statement branches the compiler won’t complain because a default value is already present.

On the other hand, relying on the default initialization value, assigning the variable only in some branches of the statement could be legit and intended. So the same construct can be correct or wrong according to a larger semantic context requiring extra effort when reading the code.

char const* f( int x ) {
  char const* result = "zero";
  if( x == 1 ) {
    printf("yeah\n");
    result="one";
  }
  else {
    printf("ouch\n");
  }
  return result;
}

Hard to tell from this example alone if the function has a bug (missing result assignment in the else branch), or is correct (default value is used for the else branch).

Here come functional statements, i.e. statements that look like their imperative counterparts, but return a value (I already wrote about these in my Is C++ ready for FP series). In C and C++ you have the ternary operator, which is a dumbed-down version of the functional if/else statement. (This is just another entry in the missed opportunity for unification in the C++ records, but this would be a too-long digression).

The problem with the ternary operator is that you can use just expressions in the then/else branches, you can’t insert other statements. You could be tempted to use the comma operator, but I would advise against it unless you are up for an obfuscated source contest (or you hate your readers). The comma operator has few applications and should be restrained to those (note that – folly madness – C++ allows you to overload the comma operator).

Therefore without employing the comma operator, you can’t translate the first example into an equivalent and terse code:

char const* f( int x ) {
  bool result = x == 1 ? "one" : "zero";
  if( x == 1 ) {
    printf("yeah\n");
  }
  else {
    printf("ouch\n");
  }
  return result;
}

Statements in languages such as Scala and Rust always return a value and, based on the context, the lack of such a feature in C++ ranges from slightly annoying to stressful. Consider the following Scala code:

def f( x: Int ) : Boolean =
  if( x == 1 ) {
    println("yeah")
    true
  }
  else {
    print("ouch")
    false
  }

Although this feature is not even on the deep scan radar of the committee, I found an interesting workaround based on lambda functions.

Considering that lambda functions are C++ (ugly) first-class citizens, you may create a temporary value for them, with no other purpose to be invoked in place:

char const* f( int x ) {
  char const* result = [&](){  // capture everything by reference, no arguments
    if( x == 1 ) {
      printf("yeah\n");
      return "one";
    }
    else {
      printf("ouch\n");
      return "zero";
    }
  }();
  return result;
}

You can improve the robustness by avoiding the default capture and having the discriminant variable as an argument:

char const* f( int x ) {
  char const* result = []( int _ ){
    if( _ == 1 ) {
      printf("yeah\n");
      return "one";
    }
    else {
      printf("ouch\n");
      return "zero";
    }
  }( x );
  return result;
}

Note that this version gives the reader a better understanding of what is going on because the lambda pretty well defines a fence around its content, the only input is x, and the only output is what lambda returns. You don’t always manage to achieve this kind of terseness – after all, this is a silly example -, but you would always have a clear list of which are the dependencies of that code fragment.

Eventually, all C++ programmers would ask the price in terms of additional CPU cycles for the increase of the abstraction level. Time for compiler explorer. As you can see in https://godbolt.org/z/bhescexKc, the lambda version generates the very same assembly code as the imperative version.

The only drawback is that the code is a bit awkward (at least the first time), but when did awkwardness stop any C++ programmer?

After a few weeks, since I wrote this post, my dear colleague Phuong brought to my attention the std::invoke function. This function invokes its argument and returns the results. This is very convenient to invoke lambdas without the need for the trailing parenthesis:

char const* f( int x ) {
  char const* result = std::invoke( [&](){
    if( x == 1 ) {
      printf("yeah\n");
      return "one";
    }
    else {
      printf("ouch\n");
      return "zero";
    }
  });
  return result;
}

If the lambda has any argument, then they have to be passed to invoke after the lambda itself, increasing the awkwardness once again.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.