Skip to content

Instantly share code, notes, and snippets.

@revivalizer
Last active August 24, 2018 18:07
Show Gist options
  • Save revivalizer/fb6dbf1277fe1a29a308 to your computer and use it in GitHub Desktop.
Save revivalizer/fb6dbf1277fe1a29a308 to your computer and use it in GitHub Desktop.
In defence of pipable functions in C++

Chaining function calls via the dot operator is a very common thing to do in C++. Of course, you chain things because it's nice, and easier to read than nested function calls. The prototypical use case is matrix composition:

auto m = Matrix::Indentity().RotateX(145.0).RotateY(-90.0).Scale(2.0);

Which seems preferable to

auto m = Scale(RotateY(RotateX(Matrix::Identity, 145.0), -90.0), 2.0).

Usually, dot chaining is implemented by return *this from Scale, RotateX, RotateY, and having those functions be part of the Matrix class. This is usually a brilliant way to work.

However, there is one problem with this approach: It creates a very tight coupling between the underlying data and the operations you can perform on them. In some cases, such as for the Matrix class, that is completely fine.

But in other cases, the coupling makes less sense.

My current use case is a postprocessing system using shaders, operating on the GPU. Here is some actual postprocessing code that is applied after rendering:

#include "Texture.h"

Texture final =
  color
  ->Blend(renderer, ao, ZPPTexture::kBlendMixAO)
  ->Glow(renderer, R(glow_scale), 0.707f, 5, ...)
  ->Glow(renderer, R(glow2_scale), 0.707f, 5, ...)
  ->ToneMap(renderer, R(exp), zifloord(R(mode)))
  ->Vignette(renderer);

This works great. But the tight coupling of postprocessing functions with the Texture class makes less sense here. One way to tell, is that the reusability of this class is a lot lower than the reusability of the Matrix class. Who's to say that different project will need exactly the same post processing functions?

There might also be potentially hundreds of postprocessing methods. It seems inelegant that all of those should be in the Texture class. Also, there is no way for me to pick and choose. I either get all of them or none of them.

All of this tells me that this design is bad.

  1. The coupling between data and operations is too tight
  2. It's not reusable
  3. I want to pick and choose what operations to include in my projects

Enter pipable functions. Pipeable functions exist outside the class, can be reused and included as needed, and they support chaining via the pipe operator.

#include "Texture.h"
#include "Blend.h"
#include "Glow.h"
#include "Tonemap.h"
#include "Vignette.h"

Texture final =
  color
  | Blend(renderer, ao, ZPPTexture::kBlendMixAO)
  | Glow(renderer, R(glow_scale), 0.707f, 5, ...)
  | Glow(renderer, R(glow2_scale), 0.707f, 5, ...)
  | ToneMap(renderer, R(exp), zifloord(R(mode)))
  | Vignette(renderer);

The underlying implementation might not be pretty, but it seems usable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment