Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Encapsulating a large number of parameters in C++14

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
like image 702
Daniel Avatar asked Feb 27 '18 01:02

Daniel


People also ask

How do you avoid too many parameters in a function?

There are two techniques that can be used to reduce a functions' arguments. One of them is to refactor the function, making it smaller, consequently, reducing the arguments' number. The Extract Method technique can be use to achieve this goal.

How many parameters can be passed in C?

Neither the C nor C++ standard places an absolute requirement on the number of arguments/parameters you must be able to pass when calling a function, but the C standard suggests that an implementation should support at least 127 parameters/arguments (§5.2.

What is a function definition in C++?

A function is a block of code that performs some operation. A function can optionally define input parameters that enable callers to pass arguments into the function. A function can optionally return a value as output.


3 Answers

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.

like image 153
Walter Avatar answered Oct 16 '22 21:10

Walter


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.

like image 26
Stephen Newell Avatar answered Oct 16 '22 21:10

Stephen Newell


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.

like image 2
einpoklum Avatar answered Oct 16 '22 19:10

einpoklum