Named Parameters in C++20
07 Sep 2020A programming language supports named parameters when one can call a function supplying the parameters by name, as in the following hypothetical example (using C++ syntax):
void f( int x, int y );
int main()
{
f( x = 1, y = 2 );
}
C++ is obviously not such a language and there have been numerous proposals to rectify this omission, unfortunately none of them successful. The latest attempt is Axel Naumann’s paper Self-explanatory Function Arguments, which tries to attack the problem from another angle by just allowing normal function calls to be tagged with the parameter name, as in
f( x: 1, y: 2 );
enabling compilers to issue helpful warnings when a name doesn’t match, but not allowing one to omit, or reorder, arguments.
Even in this limited form, named parameters would still be immensely useful, but this is not what this post is about. What this post is about is that we can already achieve something very close to named parameters in C++20, by using a C99 feature called designated initializers.
Designated initializers allow one to initialize structures by member name, as in the following example:
struct A
{
int x;
int y;
};
A a1 = { .x = 1, .y = 2 };
A a2 = { .x = 3 }; // a2.y == 0
A a3 = { .y = 4 }; // a3.x == 0
A a4 = { .y = 5, .x = 6 }; // valid C, invalid C++ (reorder)
C++ introduces a restriction C doesn’t have: the initializers must follow the declaration order, similarly to how class member initalizers are executed in member declaration order. But in exchange, it allows us to supply default values:
struct A
{
int x = 0;
int y = 0;
};
A a3 = { .y = 4 }; // a3.x == 0, no warning
You can already see where this is going. Instead of
void f( int x, int y );
we declare
void f( A args );
and then call it like this:
int main()
{
f({ .x = 1, .y = 2 });
}
This works under GCC and
Clang even without -std=c++20
because
they support designated initializers in earlier language modes as
an extension, and it works under MSVC
with -std:c++latest
.
For a more realistic example, consider this snippet, taken from real code, that sets a 10 second timeout on a Boost.Beast websocket:
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <chrono>
void f1(boost::beast::websocket::stream<boost::beast::tcp_stream>& ws)
{
auto opt = boost::beast::websocket::stream_base::timeout();
opt.keep_alive_pings = true;
opt.idle_timeout = std::chrono::seconds(10);
ws.set_option(opt);
}
Here’s how we can reformulate it by using the above idiom and <chrono>
literals:
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <chrono>
using namespace std::chrono_literals;
void f2(boost::beast::websocket::stream<boost::beast::tcp_stream>& ws)
{
ws.set_option({ .idle_timeout = 10s, .keep_alive_pings = true });
}
Apart from the slightly awkward ({ ... })
syntax and the need to observe
the right parameter order, that’s not that far from the ideal; and it’s
considerably better than f1
.
This also works for constructors. Consider this hypothetical vector
class
that is like std::vector
, except with its various constructor overloads
replaced with one taking named parameters:
template<class T, class A = std::allocator<T>> class vector
{
private:
struct params
{
std::size_t size = 0;
T element{};
std::size_t capacity = 0;
A allocator{};
};
public:
explicit vector( params p );
};
This is how it’s used:
auto f()
{
vector<int> v{{ .size = 4, .element = 11, .capacity = 64 }};
return v;
}
Again, apart from the odd {{ ... }}
syntax, not that bad.