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?
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);
[computer](int input){ return computer.compute(input); }
struct ComputeParams {
int a, b, c;
};
int compute(const ComputeParams ¶ms, int input) {
// do something with params.a, params.b, params.c
}
...
const ComputeParams params{a, b, c};
int result = compute(params, 123);
compute
involves calling params.a
instead of a
.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);
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);
auto
or template magic to inline the lambda function, or std::function
with a performance overheadThere 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.
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.
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.
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.
A lot of this is opinion-based, but I'll throw my hat in the ring.
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.
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.
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.
tl;dr: Use the code snippet below.
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.
Similar criticism as for the supposedly-object-oriented option. Just sticking a functor in there without good reason.
You're really just using a lambda instead of constructing the struct... not terrible but see the next option:
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 int
s).
This is what I'd recommend.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With