📜 ⬆️ ⬇️

Named Parameters in Modern C ++

From Wikipedia: “ Named parameters in programming languages ​​mean support for specifying explicit parameter names in a function call. A function call that takes named parameters differs from a normal function call, in which the arguments passed are associated with the function parameters only in their order in the function call

Let's look at an example:

createArray(10, 20); //   ?   "10" ?   "20" ? createArray(length=10, capacity=20); // ,   ! createArray(capacity=20, length=10); //    . 

')
And one more example in a fictional pseudo-language:
 window = new Window { xPosition = 10, yPosition = 20, width = 100, height = 50 }; 


This approach is especially useful for functions with a large number of optional parameters, which, when called, only need to change some of the default values. Some programming languages ​​support named parameters (C #, Objective-C, ...), but not C ++. In this post, we will look at a couple of classic ways of emulating named parameters in C ++, well, let's try to invent something new.


Comments


Let's start with the unreal, but the easiest way - emulate named parameters through comments :)

 Window window { 10, // xPosition 20, // yPosition 100, // width 50 // height }; 


This approach is very popular among Windows developers, as the examples in MSDN are often provided with such comments.

Idiom "named parameter"


The idea comes from Java programming style: create a proxy class that will include all optional parameters in the form of methods. After that, we can use a chain of calls to these methods to set only the parameters we need:

 // 1 File f { OpenFile{"path"} //   .readonly() .createIfNotExist() . ... }; 


 // 2   (    "   -") File f = OpenFile { ... } .readonly() .createIfNotExist() ... ; 


 // 3   "   -" -      ( CreateFile) auto f = CreateFile ( OpenFile("path") .readonly() .createIfNotExists() . ... )); 


The OpenFile class is a set of parameters, and the File constructor takes an object of this class. Some authors (for example, here ) argue that OpenFile should only have private members and declare the File class friendly. This may make sense if you want to use some more complex parameter setting logic. But for the assignment of simple values, the above style with public methods will also be fine.

In this approach:


Idiom "parameter package"


The idea is similar to the previous one and is taken from the book Davide Di Gennaro's Advanced C ++ Metaprogramming - a technique of using proxy objects for setting parameters through an assignment operator (=), in the end we get the following syntactic sugar:

 MyFunction(begin(v), end(v), where[logger=clog][comparator=greater<int>()]); 


Involved entities:


In characters:

 where = {a, b, c } where[logger = x] → { a,b,c }[ argument<0>(x) ] → {x,b,c} 


Implementation Sketch:

 // argument template <size_t CODE, typename T = void> struct argument { T arg; argument(const T& that) : arg(that) { } }; // void argument - just to use operator= template <size_t CODE> struct argument<CODE, void> { argument(int = 0) { } template <typename T> argument<CODE, T> operator=(const T& that) const { return that; } argument<CODE, std::ostream&> operator=(std::ostream& that) const { return that; } }; // " " ( ) template <typename T1, typename T2, typename T3> struct argument_pack { T1 first; T2 second; T3 third; argument_pack(int = 0) { } argument_pack(T1 a1, T2 a2, T3 a3) : first(a1), second(a2), third(a3) { } template <typename T> argument_pack<T, T2, T3> operator[](const argument<0, T>& x) const { return argument_pack<T, T2, T3>(x.arg, second, third); } template <typename T> argument_pack<T1, T, T3> operator[](const argument<1, T>& x) const { return argument_pack<T1, T, T3>(first, x.arg, third); } template <typename T> argument_pack<T1, T2, T> operator[](const argument<2, T>& x) const { return argument_pack<T1, T2, T>(first, second, x.arg); } }; enum { LESS, LOGGER }; const argument<LESS> comparator = 0; const argument<LOGGER> logger = 0; typedef argument_pack<basic_comparator, less<int>, std::ostream> pack_t; static const pack_t where(basic_comparator(), less<int>(), std::cout); 


For the full code, look in the original book.

Although the technique seems interesting, in practice it is difficult to make it quite comfortable and common. In the book, it was not represented at all in the solution of the problem under consideration, but as an example of the “chained” call of the operator [].

Tags


Andrzej Krzemieński published an interesting post “Intuitive Interface” , where he proposed the following: named parameters are pairs of companions — real value and empty structure (empty structures of different types are needed to select the desired overloaded function). Here is an example of this approach from STL:

 std::function<void()> f{std::allocator_arg, a}; // a -  std::unique_lock<std::mutex> l{m, std::defer_lock}; //  lock 


Andrzej proposed to generalize the approach:

 //   STL std::vector<int> v1(std::with_size, 10, std::with_value, 6); 


As you understand, you will need to create a number of overloaded functions, and you cannot choose the order of the parameters. The advantages include the absence of the need for copy / transfer constructors. Passing defaults also works without problems. From the article: “Tags are not an ideal solution, since they clutter up namespaces with overloaded functions that are useful only in a few places where they call”

In addition, one of the readers suggested a good idea for a different tag implementation:
std :: vector v1 (std :: with_size (10), std :: with_value (6));

Boost


Boost has a library of parameters .

As might be expected, this is a fairly complete and practical implementation. Example:

 //   #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), //    function_with_named_parameters, //   tag, //  "".    BOOST_PARAMETER_NAME,      "tag" (required //       (foo, (int)) (bar, (float)) ) (optional // ,    -    (baz, (bool) , false) (bonk, (std::string), "default value") ) ) { if (baz && (bar > 1.0)) return foo; return bonk.size(); } //   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"); 


Named parameters in modern C ++


Recent C ++ standards open new doors. Let's see if we can apply any of them to solve our problem.

Lambda


The chain call method is too verbose. I do not want to add a bunch of functions that return the object itself. How about defining a structure and setting its members through lambda functions?

 struct FileRecipe { string Path; //   bool ReadOnly = true; //   bool CreateIfNotExist = false; //   // ... }; class File { File(string _path, bool _readOnly, bool _createIfNotexist) : path(move(_path)), readOnly(_readOnly), createIfNotExist(_createIfNotExist) {} private: string path; bool readOnly; bool createIfNotExist; }; auto file = CreateFile( "path", [](auto& r) { // - - r.CreateIfNotExist = true; }); 


We still need a class for storing parameters, but the approach itself scales better than the classic idiom of the named parameter, in which you need to explicitly prescribe all the "chain" functions. Another option is to make a File class constructor that accepts an object of type FileRecipe.

How to improve the readability of the required parameters? Let's try to combine this approach with tags:

 auto file = CreateFile( _path, "path", [](auto& r) { r.CreateIfNotExist = true; }); 


True, they are still positional. If you allow the possibility to get an error “no required parameter” in runtime, you can use the type optional

I recently tried using this approach to configure tests and mocks. For example, I needed to create tests for a simple dice game. The configuration and tests used to look like this:

 TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { GameConfiguration gameConfig { 5u, 6, 2u }; } 


Using this approach, they may look like this:

 TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CreateGameConfig( [](auto& r) { r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; }); } 


We can also use macros in order not to repeat in each test with the call of the same lambda:

 TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CREATE_CONFIG( r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; ); } 


Using Variadic Templates


Appeared in C ++ 11 Variadic Templates can improve the method described above. Let's remember the tags again. Tags can be a better approach than lambda + object parameters, since we don’t need to create another object, there are no problems with copy constructors, all parameters are processed uniformly (with lambdas we had to handle the required parameters differently). But tags can be a good enough approach only if we had:


Something like:

 File f { _readonly, true, _path, "some path" }; 


or:

 File f { by_name, Args&&... args) {} 


My idea is as follows: I want to use Variadic Templates to allow the user to determine the order of the parameters and omit the optional parameters.

Imagine two constructors:

 File(string path, bool readonly, bool createIfNotExist) {} //    template<typename... Args> File(by_name_t, Args&&... args) {} 


An object of type File can be created in either of two ways. If you use the second constructor, it will look at all the parameters in the set and call the first constructor with the appropriate set of parameters. Parameter viewing and code generation is performed at the compilation stage, it takes linear time and does not affect the time spent on a call in runtime.

This implementation is only a sketch, for sure it can be improved.

Here's how a class can be designed:

 File(string path, bool readonly, bool createIfNotExists /*...*/) : _path (move(path)), _createIfNotExist(createIfNotExist), _readonly(readonly) // ,etc... { } template<typename Args...> File(named_tag, Args&&... args) : File{ REQUIRED(path), OPTIONAL(read, false) // , etc... } //  { } 


Before showing you the working code, let's make it clear that we can apply the same idea to a proxy:

 auto f = File { by_name, readonly=true, path="path" }; 


The main difference here is in the transfer of arguments: with a proxy, we get syntactic sugar (operator =), but now we need to store and transfer values ​​(not very good for non-relocatable / replicable types).

Here you can experiment with the code. I started with the tagged version and then switched to the proxy, so both versions are there. You will find two sections called “PACK UTILS” (for tags and proxy).

Here is what the class will look like:

 class window { public: //   window( string pTitle, int pH, int pW, int pPosx, int pPosy, int& pHandle) : title(move(pTitle)), h(pH), w(pW), posx(pPosx), posy(pPosy), handle(pHandle) { } // ,   (_title = "title") template<typename... pack> window(use_named_t, pack&&... _pack) : window { REQUIRED_NAME(title), // required OPTIONAL_NAME(h, 100), // optional OPTIONAL_NAME(w, 400), // optional OPTIONAL_NAME(posx, 0), // optional OPTIONAL_NAME(posy, 0), // optional REQUIRED_NAME(handle) } // required { } // ,   (__title, "title") template<typename... pack> window(use_tags_t, pack&&... _pack) : window { REQUIRED_TAG(title), // required OPTIONAL_TAG(h, 100), // optional OPTIONAL_TAG(w, 400), // optional OPTIONAL_TAG(posx, 0), // optional OPTIONAL_TAG(posy, 0), // optional REQUIRED_TAG(handle) } // required { } private: string title; int h, w; int posx, posy; int& handle; }; 


As you can see, both of the latest constructors always call the “classic” constructor to do the actual work.

The following piece of code shows how a user can create an object:

 int i=5; //    window w1 {use_tags, __title, "Title", __h, 10, __w, 100, __handle, i}; cout << w1 << endl; //    window w2 {use_named, _h = 10, _title = "Title", _handle = i, _w = 100}; cout << w2 << endl; //   window w3 {"Title", 10, 400, 0, 0, i}; cout << w3 << endl; 


Pros:



Minuses:


Pay attention to the first problem: Clang is smart enough to report a problem quite clearly. Imagine that I forgot about the obligatory parameter with the name of the window, here is the output of the compiler:

 main.cpp:28:2: error: static_assert failed "Required parameter" static_assert(pos >= 0, "Required parameter"); ^ ~~~~~~~~ main.cpp:217:14: note: in instantiation of template class 'get_at<-1, 0>' requested here : window { REQUIRED_NAME(title), ^ 

Now you know exactly what was missing and where.

Minimalist approach using std :: tuple


[this paragraph was written by Davide Di Gennaro]

We can use the tuple functional (std :: tuple) to write a very compact and portable implementation of our task. We will rely on a few simple principles:



 namespace tag { CREATE_TAG(age, int); CREATE_TAG(name, std::string); } template <typename pack_t> void MyFunction(T& parameter_pack) { int myage; std::string myname; bool b1 = extract_from_pack(tag::name, myname, parameter_pack); bool b2 = extract_from_pack(tag::age, myage, parameter_pack); assert(b1 && myname == "John"); assert(b2 && myage == 18); } int main() { auto pack = (tag::age=18)+(tag::name="John"); MyFunction(pack); } 


This is how the implementation of this idea might look.

First macro:

 #include <tuple> #include <utility> template <typename T> struct parameter {}; #define CREATE_TAG(name, TYPE) \ \ struct name##_t \ { \ std::tuple<parameter<name##_t>, TYPE> operator=(TYPE&& x) const \ { return std::forward_as_tuple(parameter<name##_t>(), x); } \ \ name##_t(int) {} \ }; \ \ const name##_t name = 0 


Expanding the macro CREATE_TAG (age, int) creates a class and a global object.

 struct age_t { std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::forward_as_tuple(parameter<age_t>(), x); } age_t(int) {} }; const age_t age = 0; 



Conceptually assignment

 age = 18 


Transformed into something like:

 make_tuple(parameter<age_t>(), 18); 


Please note that we wrote:

 std::tuple<parameter<age_t>, int> operator=(int&& x) const 


We require r-value on the right. This is done for the sake of security: for the sake of improved code readability with parameter sets, you may want to assign constants, not variables.

 int myage = 18; f(myage); // ok g((...) + (age=18)); // ok g((...) + (age=myage)); //  ,        


In addition, we can use the move semantics:

Difference between
 std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::make_tuple(parameter<age_t>(), x); } 


and

 std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::forward_as_tuple(parameter<age_t>(), x); } 


very thin. In the latter case, it returns std :: tuple <..., int &&>, but since the function returns std :: tuple <..., int>, the displacement constructor std :: tuple is called.

As an alternative, we could write:

 std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::make_tuple(parameter<age_t>(), std::move(x)); } 


And now we write the appropriate concatenation operator for our tuples.

We implicitly agree that all tuples that start with a parameter were created by our code, so without any explicit validation, we simply discard the parameter.

 template <typename TAG1, typename... P1, typename TAG2, typename... P2> std::tuple<parameter<TAG1>, P1..., parameter<TAG2>, P2...> operator+ (std::tuple<parameter<TAG1>, P1...>&& pack1, std::tuple<parameter<TAG2>, P2...>&& pack2) { return std::tuple_cat(pack1, pack2); } 


Very simple function: checks that both tuples have the form

 tuple<parameter<tag>, type, [maybe something else]> 


and connects them.

Finally, we will write the function to extract the argument from the set. Please note that this function has transfer semantics (i.e., after its call, the parameter will be retrieved from the set).

 template <typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack); 


It works as follows: if the set contains a parameter, then the variable gets the value immediately following it and the function returns true. Otherwise, something bad happens (we can choose a compilation error, return false, throw an exception).

To make this choice possible, the function will look like:

 template <typename ERR, typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack) 


and we will call it like this:
 extract_from_pack< erorr_policy > (age, myage, mypack); 


In view of the rules for working with variadic templates, extract_from_pack knows that the parameter set has the form tuple <parameter, ...>, so you need to check recursively whether TAG is really TAG1. We implement this by calling the class:

 extract_from_pack< erorr_policy > (age, myage, mypack); 


causes

 extractor<0, erorr_policy >::extract (age, myage, mypack); 


which further causes

 extractor<0, erorr_policy >::extract (age, myage, std::get<0>(pack), mypack); 


which has two overloaded options:

 extract(TAG, … , TAG, …) 


which, if executed, performs the assignment and returns true or

 extract(TAG, … , DIFFERENT_TAG, …) 


which continues the iteration, calling again

 extractor<2, erorr_policy >::extract (age, myage, mypack); 


when the continuation of the iteration is not possible, error_policy :: err (...) is called

 template <size_t N, typename ERR> struct extractor { template <typename USERTAG, typename T, typename TAG, typename... P> static bool extract(USERTAG tag, T& var, std::tuple<parameter<TAG>, P...>&& pack) { return extract(tag, var, std::get<N>(pack), std::move(pack)); } template <typename USERTAG, typename T, typename TAG, typename... P> static bool extract(USERTAG tag, T& var, parameter<TAG> p0, std::tuple<P...>&& pack) { return extractor<(N+2 >= sizeof...(P)) ? size_t(-1) : N+2, ERR>::extract(tag, var, std::move(pack)); } template <typename USERTAG, typename T, typename... P> static bool extract(USERTAG tag, T& var, parameter<USERTAG>, std::tuple<P...>&& pack) { var = std::move(std::get<N+1>(pack)); return true; } }; template <typename ERR> struct extractor<size_t(-1), ERR> { template <typename TAG, typename T, typename DIFFERENT_TAG, typename... P> static bool extract(TAG tag, T& var, std::tuple<parameter<DIFFERENT_TAG>, P...>&& pack) { return ERR::err(tag); } }; template <typename ERR, typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack) { return extractor<0, ERR>::extract(tag, var, std::move(pack)); } 


In view of the flexible nature of the parameter sets, the “return false” error can be considered the best policy to handle (any more strict behavior will in fact mean that each parameter is mandatory).

 struct soft_error { template <typename T> static bool err(T) { return false; } }; 


However, if for some reason you need, we can also choose from these two:

 struct hard_error { template <typename T> static bool err(T); //  ,  static_assert(false)   .     ? }; struct throw_exception { template <typename T> static bool err(T) { throw T(); return false; } }; 


An additional improvement could be a redundancy check for such cases as:
 (age=18)+(age=19) 


Final Notes


We did not discuss the runtime technique, like:

 void MyFunction (option_parser& pack) { auto name = pack.require("name").as<string>(); auto age = pack.optional("age", []{ return 10; }).as<int>(); ... } 


The code works on runtime, trying to get the parameters it needs in the course of work, respectively, we have a lot of time, well, you will learn about the error only when it occurs. The code is far from ideal, I cite it only as a “proof of concept” and I do not think that in this form it can be used in real projects.

I also found the sentence to add named parameters to the C ++ standard here . It would be great if.

Source: https://habr.com/ru/post/246663/


All Articles