6

I want to write a function which uses many parameters, which I will call a, b, and c. I have four choices of implementing this in C++14.

For a new modern C++ project in 2018, which one of these styles best adheres to the philosophy of the ISO C++? Which styles are recommended by other style guides?

Object oriented style

class Computer {
    int a, b, c;
public:
    Computer(int a, int b, int c) : a(a), b(b), c(c) {}
    int compute(int) const {
        // do something with a, b, c
    }
};
...
const Computer computer(a, b, c);
int result = computer.compute(123);

Pros:

  • Easy for C++ programmers to grasp

Cons:

  • To compute things in map or fold operations, we have to do the clunky [computer](int input){ return computer.compute(input); }

C style

struct ComputeParams {
    int a, b, c;
};

int compute(const ComputeParams &params, int input) {
    // do something with params.a, params.b, params.c
}
...
const ComputeParams params{a, b, c};
int result = compute(params, 123);

Pros:

  • Easy for C programmers to grasp

Cons:

  • Verbose implementation of compute involves calling params.a instead of a.
  • Verbose calling, have to pass in a struct every time.

Functor style

struct Computor {
    int a, b, c;
    int operator()(int input) const {
        // do something with a, b, c
    }
};
...
const Computor compute{a, b, c};
int result = compute(123);

Pros:

  • All the advantages of object oriented style, plus it looks like a function
  • Can use in functional operations such as map, fold, and for_each

Cons:

  • The word "functor" looks funky.

Functional style

auto genCompute(int a, int b, int c) {
    return [a, b, c](int input) -> int {
        // do something with a, b, c
    }
}
...
auto compute = genCompute(a, b, c);
int result = compute(123);

Pros:

  • Easy for OCaml programmers to grasp
  • Can use in functional operations such as map, fold, and for_each
  • Technically the same as a functor

Cons:

  • Hard for C++ and C programmers to grasp
  • Since lambda functions are a unique type that's generated by the compiler, may require use of auto or template magic to inline the lambda function, or std::function with a performance overhead
  • Can't embrace the power of vtables and inheritance for polymorphism
Walter
  • 44,150
  • 20
  • 113
  • 196
Daniel
  • 510
  • 3
  • 15
  • Note that [with uniform function call (pdf)](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0251r0.pdf), the C style and Object Oriented style above become the same thing. – Daniel Feb 27 '18 at 02:16
  • The "object oriented" and "functor" are same. Just add operator() and make "doIt" or "compute" method as "private virtual" called from (non- virtual) operator() of base class when dynamically polymorphic callables are needed. OTOH why function must be in global namespace? it is better not to put *any* names into global namespace. – Öö Tiib Feb 27 '18 at 07:30
  • You are right regarding namespaces; everything should be in their own namespace. My point was that a class member gets a sort of "namespace" for free. – Daniel Feb 27 '18 at 20:07

3 Answers3

4

There is more to say in favour of functional style: you can easily provide special/optimized versions for certain parameter values

std::function<int(int)> getCompute(int a, int b, int c)
{
    if(a==0)
        return [b,c](int input) { /* version for a=0 */ };
    if(b==0)
        return [a,c](int input) { /* version for b=0 */ };
    if(c==0)
        return [a,b](int input) { /* version for c=0 */ };
    /* more optimized versions */
    return [a,b,c](int input) { /* general version */ };
}

Something equivalent is not straightforward with the other options. Unfortunately, this requires the use of std::function to wrap the different lambdas.

Walter
  • 44,150
  • 20
  • 113
  • 196
  • 1
    Not sure you can still return auto in your example since each lambda is a different type (I've been bitten by this), but you could return an std::function. Nice observation though. – Stephen Newell Feb 28 '18 at 00:45
  • @StephenNewell I wondered about that myself, but you're correct. – Walter Feb 28 '18 at 00:47
2

A lot of this is opinion-based, but I'll throw my hat in the ring.

Object oriented style

Not a fan for your use. Since the only operation you support is compute, it's effectively operator () by a different name. operator () means you play nicely with the algorithm header, so this is an inferior solution to the Functor and Functional styles.

In addition, you may have code that performs more poorly with this solution. The entire talk is worth watching if you're curious, but Chandler Carruth (an LLVM/Clang developer) explains how the compiler sees your code (skip to about 1:32:37, but the whole talk is great). The gist of it is that you have an implicit pointer in this implementation, and pointers/references are much harder for the compiler to optimize.

C style

Not a fan of this just for the sake of the API. You mentioned in your cons that calling requires passing along the struct, and that's a problem when working with libraries that want a single thing to operate on (e.g., everything in algorithm). You can get around this using a lambda that captures your struct, but at that point I don't know what you gain.

Functional Style

This is the way I'd go and what I'm pushing at work. Examples I've seen show that calling a lambda function is no slower than calling a function directly since compilers can aggressively inline (they know the exact type). If C++ programmers struggle with this style because it's different/new, tell them to get up to speed since they're several standards behind :).

As far as best practices and what the community is using, examples from Cppcon seem to be geared more toward functor/functional style. C++ as a language looks like it's really embracing functional design in general.

Stephen Newell
  • 7,330
  • 1
  • 24
  • 28
  • What is the performance difference between the 'Object oriented style' and the 'Functional Style'? Don't lambdas just get converted into an equivalent struct/class to the one used in the Functor/Object Oriented style (internally to the compiler)? – Mankarse Feb 28 '18 at 00:47
  • 2
    I had a quick look using the [Compiler Explorer](https://godbolt.org/), and `computer.compute(123);` had identical code to `compute(123);`; as did the relevant compute/lambda function. The only difference was a slight calling convention difference in the generated code between calling a constructor and calling a function which created a lambda or default-ctor struct. – Mankarse Feb 28 '18 at 01:14
2

tl;dr: Use the code snippet below.

The supposedly-Object-oriented option

It's not object-oriented, it's just sticking things in an object. A function which "does something with a b and c" is not supposed to be a member of the object containing them; that function is not inherent to the combination of an a, a b and a c.

The Functor option

Similar criticism as for the supposedly-object-oriented option. Just sticking a functor in there without good reason.

The functional option

You're really just using a lambda instead of constructing the struct... not terrible but see the next option:

The supposedly-C-style option - for the win

This is very much in style in C++. A struct is a perfectly nice object - with public data members and defaulted constructors and destructor.

Also, both your cons can be easily addressed, and the code made shorter and simpler:

struct ComputeParams {
    int a, b, c;
};

auto compute(ComputeParams params, int input) {
    auto [a, b, c] = params;
    // do something with a, b and c
}
auto result = compute(ComputeParams{a, b, c}, 123);

This is valid C++17 (using structured binding); in C++14 you would need std::tie to bind the parameters to local names. Also, note I'm using more value semantics over reference semantics, letting the compiler do its magic (which it probably will; even if it doesn't matter much for a few ints).

This is what I'd recommend.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Structured bindings are indeed really great! Finally we don't have to do `std::tie(...)` like in C++14. As for functors and functional, one advantage is to be able to put them inside a `std::function` as well as use them in various STL functions such as using them as a comparator in `std::sort`. – Daniel Feb 28 '18 at 02:55
  • @Daniel: About putting things in an `std::function` - just use a lambda on the spot when you need (e.g. for passing an `std::function` somewhere); no need for the `getCompute()` generally. No need to complicate your life apriori. – einpoklum Feb 28 '18 at 10:02