While trying to figure out the optimal ordering for optional parameters to a function recently, I stumbled across this blog post and accompanying GitHub repo, which provides a header for a Pythonic kwargs
-like facility in C++. Though I didn't end up using it, I find myself wondering whether this is a good or not in a strongly-typed language. Having worked in Python for a while, I find the notion of a kwargs
-like facility in my project very appealing because many of its objects/functions have a number of optional parameters (which cannot be avoided, unfortunately), yielding long lists of constructors that differ by one or two parameters and could be made much more succinct/DRY-ish.
What, if anything, is others' experience with stuff like this? Should it be avoided? Are there guidelines for it? What are the potential problems/pitfalls?
-
You may find interesting N4172 (take a look at the objections) and Bring named parameters in modern C++.manlio– manlio2016年08月31日 09:27:08 +00:00Commented Aug 31, 2016 at 9:27
1 Answer 1
I'm not very much familiar with C++ kwargs but a couple of disadvantages come to mind after skimming their source:
- It's a third-party library. Kind of obvious, but still you need to figure a way to integrate it into your project and update the source when the original repo is changed.
They require globally pre-declaring all arguments. The simple example on the blog post has this section which is dead weight:
#include "kwargs.h" // these are tags which will uniquely identify the arguments in a parameter // pack enum Keys { c_tag, d_tag }; // global symbols used as keys in list of kwargs kw::Key<c_tag> c_key; kw::Key<d_tag> d_key; // a function taking kwargs parameter pack template <typename... Args> void foo(int a, int b, Args... kwargs) { // first, we construct the parameter pack from the parameter pack kw::ParamPack<Args...> params(kwargs...); ...
Not as concise as the pythonic original.
- Potential binary bloat. Your function needs to be a variadic template, so each permutation of parameters will generate the binary code anew. The compiler is often unable to see they differ in trivialities and merge the binaries.
- Slower compilation times. Again, your function needs to be a template and the library itself is template-based. There's nothing wrong with templates but compilers need time to parse and instantiate them.
C++ offers native alternatives to achieve the functionality of named parameters:
Struct wrappers. Define your optional parameters as fields of a struct.
struct foo_args { const char* title = ""; int year = 1900; float percent = 0.0; }; void foo(int a, int b, const foo_args& args = foo_args()) { printf("title: %s\nyear: %d\npercent: %.2f\n", args.title, args.year, args.percent); } int main() { foo_args args; args.title = "foo title"; args.percent = 99.99; foo(1, 2, args); /* Note: in pure C brace initalizers could be used instead but then you loose custom defaults -- non-initialized fields are always zero. foo_args args = { .title = "foo title", .percent = 99.99 }; */ return 0; }
Proxy objects. Arguments are stored in a temporary struct which can be modified with chained setters.
struct foo { // Mandatory arguments foo(int a, int b) : _a(a), _b(b) {} // Optional arguments // ('this' is returned for chaining) foo& title(const char* title) { _title = title; return *this; } foo& year(int year) { _year = year; return *this; } foo& percent(float percent) { _percent = percent; return *this; } // Do the actual call in the destructor. // (can be replaced with an explicit call() member function // if you're uneasy about doing the work in a destructor) ~foo() { printf("title: %s\nyear: %d\npercent: %.2f\n", _title, _year, _percent); } private: int _a, _b; const char* _title = ""; int _year = 1900; float _percent = 0.0; }; int main() { // Under the hood: // 1. creates a proxy object // 2. modifies it with chained setters // 3. calls its destructor at the end of the statement foo(1, 2).title("foo title").percent(99.99); return 0; }
Note: the boilerplate can be abstracted out to a macro at the expense of readability:
#define foo_optional_arg(type, name, default_value) \ public: foo& name(type name) { _##name = name; return *this; } \ private: type _##name = default_value struct foo { foo_optional_arg(const char*, title, ""); foo_optional_arg(int, year, 1900); foo_optional_arg(float, percent, 0.0); ...
Variadic functions. This obviously is type-unsafe and requires knowledge of type promotions to get right. It is, however, available in pure C if C++ is not an option.
#include <stdarg.h> // Pre-defined argument tags enum foo_arg { foo_title, foo_year, foo_percent, foo_end }; void foo_impl(int a, int b, ...) { const char* title = ""; int year = 1900; float percent = 0.0; va_list args; va_start(args, b); for (foo_arg arg = (foo_arg)va_arg(args, int); arg != foo_end; arg = (foo_arg)va_arg(args, int)) { switch(arg) { case foo_title: title = va_arg(args, const char*); break; case foo_year: year = va_arg(args, int); break; case foo_percent: percent = va_arg(args, double); break; } } va_end(args); printf("title: %s\nyear: %d\npercent: %.2f\n", title, year, percent); } // A helper macro not to forget the 'end' tag. #define foo(a, b, ...) foo_impl((a), (b), ##__VA_ARGS__, foo_end) int main() { foo(1, 2, foo_title, "foo title", foo_percent, 99.99); return 0; }
Note: In C++ this can be made type-safe with variadic templates. The run-time overhead will be gone at the expense of slower compilation times and binary bloat.
boost::parameter. Still a third-party library, albeit more established lib than some obscure github repo. Drawbacks: template-heavy.
#include <boost/parameter/name.hpp> #include <boost/parameter/preprocessor.hpp> #include <string> BOOST_PARAMETER_NAME(foo) BOOST_PARAMETER_NAME(bar) BOOST_PARAMETER_NAME(baz) BOOST_PARAMETER_NAME(bonk) BOOST_PARAMETER_FUNCTION( (int), // the return type of the function, the parentheses are required. function_with_named_parameters, // the name of the function. tag, // part of the deep magic. If you use BOOST_PARAMETER_NAME you need to put "tag" here. (required // names and types of all required parameters, parentheses are required. (foo, (int)) (bar, (float)) ) (optional // names, types, and default values of all optional parameters. (baz, (bool) , false) (bonk, (std::string), "default value") ) ) { if (baz && (bar > 1.0)) return foo; return bonk.size(); } int main() { function_with_named_parameters(1, 10.0); function_with_named_parameters(7, _bar = 3.14); function_with_named_parameters( _bar = 0.0, _foo = 42); function_with_named_parameters( _bar = 2.5, _bonk= "Hello", _foo = 9); function_with_named_parameters(9, 2.5, true, "Hello"); }
On a closing note, I wouldn't use this kwargs library simply because there is a number of good enough alternatives in C++ to achieve the same. I personally would opt for 1. or 2. from the (non-exhaustive) list above.
-
Great answer! Out of curiosity, for approach 2, why are the internal variables
private
? Making thempublic
means they can either call the function or set the variable directly.svenevs– svenevs2017年09月21日 23:10:47 +00:00Commented Sep 21, 2017 at 23:10 -
@sjm324, thanks. Because
struct foo
is a throw-away object just to mimic the original Python function syntax; passing name-values in one line at the call site. They could bepublic
but that just wasn't the point here.An Owl– An Owl2017年11月19日 16:28:16 +00:00Commented Nov 19, 2017 at 16:28 -
-
another problem that comes to mind is that the code is a lot harder to read and comprehend for experienced C++ programmers than normal code. I've worked on a program where someone had thought it a good idea to do something like #define PROCEDURE void #define BEGIN { #define END } etc. etc. because he wanted to make C look like Pascal. Say again?jwenting– jwenting2018年08月16日 04:57:46 +00:00Commented Aug 16, 2018 at 4:57
-
1Nice answer. But it does beg the question as to why after all these years C++ still cannot do this. Particularly for bools. foo(happy:=true, fast:=false) is much easier to follow than foo(true, false). (Using Visual Basic notation here!).Tuntable– Tuntable2019年03月26日 07:40:21 +00:00Commented Mar 26, 2019 at 7:40