2

I've been using a "pattern" to configure C++ class instances via lambdas for a while now and I am looking for:

  1. an original source & name (I think I read this somewhere, but can't find any online resource unfortunately)
  2. pitfalls when using the pattern and whether there are means of solving them (I hope this question is not too vague)

The pattern basically works by passing a lambda to the constructor of a class. The class constructor then simply passes the lambda the state to be adjusted.

Example usage

What a users calling code looks like:

Configurable instance([](auto& opts) {
    opts.option = true;
});

Pattern

What a class using the "pattern" looks like:

class Configurable {
private:
    struct Opts {
        /// options that can have sane defaults, should have them
        bool option{false};
        /// options which do not have sane defaults should be an optional
        std::optional<size_t> counter;
    } opts;

public:
    Configurable(std::function<void(Opts&)> f) { 
        f(opts);

        /// options which are required to be set, can be checked via e.g. an assertion
        assert(opts.counter && "[Configurable::Configurable] counter must be set");
    }
};

Pros

  • Easily extendable with new options without breaking the client code interface (i.e. code can stay the same, but recompilation may be required)
  • Value associated with a named parameter (opts.option = value)
  • Partial configurators / combined configurators possible (e.g. Configurable(juxt(setOptionA(value1), setOptionB(value2))) where juxt is the juxtaposition, i.e. [f1, ..., fn] combined to be applied to the same argument in sequence)
    • this in particular is worth gold, when instantiating several instances with the same parameters, which is often required in the codebase I work with

Cons

  • Uncommon pattern
  • Opts struct may need additional constructor in case of non pod types
  • Overhead on classes with no / few parameters to the constructor
  • auto vs auto& in the lambda can lead to no configuration at all, even though everything compiles
2
  • "Easily extendable with new options without breaking the interface" How do you define "breaking the interface"? The caller will need to recompile their code if you change the struct. Jul 13, 2020 at 18:27
  • Thanks, that is true! To me, recompiling is less of a concern. I care more about the caller requiring to update his code. I'll update that point to hopefully better reflect that
    – MHOOO
    Jul 13, 2020 at 19:21

1 Answer 1

3

The origins

Lambdas, std::function and std::bind were introduced in C++11, so this exact technique as such is not older than 2011.

In the title, you say that it's about configuration via lambdas. But lambda is a specific example: the configuration in your code is in reality based on a std::function. So the exact same technique works with lambdas, with functions and binding of functions, and even with classes exposing operator().

I have found a relatively recent article of 2017 in which this technique is called a configurable object. However, I think that this technique is much older. For instance, one could claim that the C++11 std::thread class is a special case of application of this technique:

Also to be known, before the functional programming was added in C++11, it was a common trick to use a class with operator() to create in a less convenient and less expressive way what is nowadays done in a lambda. And even before, it was also a usual approach to use function pointers to chose or inject some behavior at runtime. Bjarne Stroustrup himself gives an example of this technique in the 1987 edition of the C++ Programming Language, although he didn't use it to call the function from the constructor. So it's not excluded that ancestors of this technique existed before 2011.

From a design pattern point of view, the use of a function object for configuration is very similar to a strategy applied solely to initialization. The use of this patter being extremely popular, I'd wonder if nobody never used it before for initializing some objects.

The problems with this pattern

Personally, and although I could imagine that there are some relevant use-cases, I would not recommend using this technique in a systematic manner:

  • It requires the calling context, in which the lambda is defined, to know more about the internals of your class than it should. This is against the principle of the least knowledge and creates tight coupling.
  • It also weakens encapsulation: these configurable elements can no longer freely evolve. For example, suppose you have your class used in 150 different places and you want to change the representation of the opts elements: this should be an easy internal kitchen of your class, but would now require to review all the 150 cases.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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