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.
- The coupling between data and operations is too tight
- It's not reusable
- 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.