Saturday 28 September 2019

C++ craftsman named arguments

We try to compensate for lack on named arguments in so many ways. Check one the lastest takes on a subject: https://www.youtube.com/watch?v=Grveezn0zhUCppCon 2018: Richard Powell “Named Arguments from Scratch”. The recent C++20 "designated initialization" doesn't help much, as you must remember the order of fields as declared in a corresponding struct. Other people try things around it, as well, such as calling functions using curly syntax: https://www.youtube.com/watch?v=L8eeDzTWEtU, or allowing default parameters in the middle of args list https://github.com/joboccara/Defaulted. So, there's a lot, and more: https://www.codeproject.com/Articles/1171605/Named-Cpp-Function-Parameters , https://www.fluentcpp.com/2018/12/14/named-arguments-cpp/ etc.

But let's pull back. What if:
struct foo_args_t {
  foo_args_ta(int v) { args.a = v; return *this; }
  foo_args_tb(int v) { args.b = v; return *this; }
  foo_args_tc(int v) { args.c = v; return *this; }
  int a() const { return args.a; }
  int b() const { return args.b; }
  int c() const { return args.c; }
privatestruct { int a = 1; int b = 2; int c = 3; } args;
};
 
int foo(foo_args_t args) {
  return args.a() + args.b() + args.c();
}
 
int main() {
  foo_args_t args;
  args.c(44);
  args.b(43);
  args.a(42);
  return foo(args);
  // return foo(foo_args_t{}.c(44).b(43).a(42));
}
This returns 129 as expected. As an additional benefit, you could have a default value for an argument set with some non-constexpr function. Drawback is a ceremony around making up properties getter and setter. In fact, the only reason we need properties there is to make the last commented line possible. Let's stratify this to a regular struct then.
struct foo_args_t {
  int a = 1; int b = 2; int c = 3;
};
int foo(foo_args_t _) {
  return _.a + _.b + _.c;
}
 
int main() {
  foo_args_t args;
  args.c = 44;
  args.b = 43;
  args.a = 42;
  return foo(args);
}
Quite leaner. To add, this is possible in C++20:
struct foo_args_t {
  int a = 11; int b = 43; int c = 33;
};
int foo(foo_args_t _) {
  return _.a + _.b + _.c;
}
 
int main() {
  return foo({.a = 44, .c = 42});
}
Forget for a moment you must remember the order of parameters. At least, you can 1) skip arguments leaving them default even in the middle of the list, and 2) get a compiler error if you mess up the argument order. So, now that would be the compiler preventing you from messing up x and y in bar(y,x) call for void bar(float x, float y). Not bad.

Let's go further and beautify it with a macro:
#define FUNC(R,N,Args) \
  struct N##_args_t {Args;}; \
  R N(N##_args_t _ = {})
 
FUNC(int, foo, int a = 11; int b = 43; int c = 33) {
  return _.a + _.b + _.c;
}
 
int main() {
  int u = foo();
  return u + foo({.a = 44, .c = 42});
}
No syntactic overhead at all.

And, lastly — beautify it even more, getting rid of a strange semicolon.
#define EXPAND(x) x
#define FOR_EACH_1(x) x
#define FOR_EACH_2(x, y) x; y
#define FOR_EACH_3(x, y, z) x; y; z
#define FOR_EACH_4(x, y, z, u) x; y; z; u
#define FOR_EACH_5(x, y, z, u, v) x; y; z; u; v
#define FOR_EACH_6(x, y, z, u, v, w) x; y; z; u; v; w
 
#define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N())
#define FOR_EACH_NARG_(...) EXPAND(FOR_EACH_ARG_N(__VA_ARGS__))
#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N
#define FOR_EACH_RSEQ_N() 6, 5, 4, 3, 2, 1, 0
#define CONCATENATE(x,y) x##y
#define FOR_EACH_(N, ...) EXPAND(CONCATENATE(FOR_EACH_, N)(__VA_ARGS__))
#define FOR_EACH(...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), __VA_ARGS__)
 
#define FUNC(R,N,...) \
  struct N##_args_t {FOR_EACH(__VA_ARGS__);}; \
  R N(N##_args_t && _ = {})
 
FUNC(int, foo, int a = 11, int b = 43, int c = 33) {
  return _.a + _.b + _.c;
}
 
int main() {
  int u = foo();
  return u + foo({.a = 44, .c = 42});
}
If this macro is worth it or not – up for you to decide.

The other drawback is not so easy refactoring. You can't simply say «rename foo» clicking on foo inside the macro usage.

No comments:

Post a Comment