Named parameters in modern C ++
- Transfer
- Tutorial
From Wikipedia: “ Named parameters in programming languages mean support for specifying explicit parameter names in a function call. Calling a function that takes named parameters differs from a regular function call in which the passed arguments are associated with the function parameters only in their order in the function call ”
Let's look at an example:
And another example in a fictional pseudo-language:
This approach is especially useful for functions with a large number of optional parameters, when called, you need to change only 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 to emulate named parameters in C ++, well, and try to come up with something new.
Let's start with a fake, but simplest way - emulating named parameters through comments :)
This approach is very popular among Windows developers, as examples on MSDN are often provided with such comments.
The idea comes from the style of programming in Java: create a proxy class that will include all optional parameters in the form of methods. After that, we can use the chain of calls of these methods to set only the parameters we need:
The OpenFile class is a set of parameters, and the File constructor accepts 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 logic for setting parameters. But to assign simple values, the above style with public methods will do just fine.
In this approach:
The idea is similar to the previous one and is taken from Davide Di Gennaro's Advanced C ++ Metaprogramming - the technique of using proxy objects to set parameters through the assignment operator (=), as a result, we get the following syntactic sugar:
Entities involved:
In characters:
Outline of implementation:
For the full code, check out the original book.
Although the technique seems interesting, in practice it is difficult to make it reasonably comfortable and general. In the book, it was generally presented not by solving the problem we are considering, but by an example of a “chain” call to the operator [].
Andrzej Krzemieński published an interesting post “Intuitive Interface” , where he suggested 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:
Andrzej proposed a generalized approach:
As you understand, you will need to create a number of overloaded functions, and also you cannot choose the order of parameters. The pluses include the lack of the need for copy / transfer constructors. Passing defaults also works without problems. From the article: “Tags are not an ideal solution because they clog namespaces with overloaded functions that are useful only in a few places where they are called.”
In addition, one of the readers suggested a good idea for another tag implementation:
std :: vector v1 (std :: with_size (10), std :: with_value (6));
Boost has a parameter library .
As you might expect, this is a fairly complete and practical implementation. Example:
The latest C ++ language standards open up new doors. Let's see if we can apply any of them to solve our problem.
The chaining method is too verbose. I do not want to add a bunch of functions that return the object itself. How about determining the structure and setting its members through lambda functions?
We still need a class for storing parameters, but the approach itself scales better than the classical idiom of a named parameter, in which you need to explicitly register all the "chain" functions. Another option is to make a constructor of the File class that accepts an object of type FileRecipe.
How to improve the readability of required parameters? Let's try to combine this approach with tags:
True, they are still positional. If you admit the possibility of getting in runtime an error “a required parameter is missing” - you can use the optional type.
I recently tried to use this approach to configure tests and mocks. For example, I needed to create tests for a simple dice game. Configuration and tests used to look like this:
Using this approach, they can look like this:
We can also use a macro so as not to be repeated in each test with the same lambdas invoked:
Variadic Templates introduced in C ++ 11 can improve the method described above. Let's remember the tags again. Tags can be a better approach than a lambda + parameter object, since we do not need to create another object, there are no problems with copy constructors, all parameters are processed in the same way (with lambdas we had to process the required parameters differently). But tags can be a good enough approach only if we had:
Something like:
or:
My idea is this: I want to use Variadic Templates to give the user the ability to determine the order of the parameters and omit the optional parameters.
Imagine two constructors:
An object of type File can be created in either of two ways. If you use the second constructor, it will look through all the parameters in the set and call the first constructor with the corresponding set of parameters. Viewing parameters and generating code 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 just a sketch, for sure it can be improved.
Here's how a class can be designed:
Before showing you the working code, let's make it clear that we can apply the same idea to a proxy:
The main difference here is in passing arguments: with the proxy we get syntactic sugar (operator =), but now we need to store and pass values (not very good for non-movable / copied 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 proxies).
Here's what the class will look like:
As you can see, both last constructors always call the “classic” constructor to do the real work.
The following piece of code shows how the user can create an object:
Pros:
Minuses:
Pay attention to the first problem: Clang is smart enough to report a problem very clearly. Imagine that I forgot about the required parameter with the window name, here is the compiler output:
Now you know exactly what exactly and where it was missed.
[this paragraph was written by Davide Di Gennaro]
We can use the tuple functionality (std :: tuple) to write a very compact and portable implementation of our task. We will rely on a few simple principles:
Here's what the implementation of this idea might look like.
First macro:
Expanding the macro CREATE_TAG (age, int) creates a class and a global object.
Conceptually assignment
Converts to something like:
Please note that we wrote:
We require r-value on the right. This is done for security: for the sake of increasing the readability of code with parameter sets, you may want to assign constants, not variables.
In addition, we can use the semantics of displacement:
The difference between
and
very thin. In the latter case, std :: tuple <..., int &&> is returned, but since the function returns std :: tuple <..., int>, the move constructor std :: tuple is called.
As an alternative, we could write:
And now we will write a suitable concatenation operator for our tuples.
We implicitly agree that all tuples starting with parameter were created by our code, so without any explicit validation, we simply throw parameter away.
Very simple function: checks that both tuples are of the form
and connects them.
And finally, we will write a function to extract an argument from a set. Please note that this function has transfer semantics (i.e., after calling it, the parameter will be extracted from the set).
It works as follows: if the set contains parameter, then the variable receives 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:
and we will call it like this:
In view of the rules for working with variadic templates, extract_from_pack knows that the set of parameters is of the form tupleso you need to check recursively if TAG is equal to TAG1. We implement this by calling the class:
causes
which further causes
which has two overloaded options:
which, if executed, performs the assignment and returns true or
which continues the iteration, calling again
когда продолжение итерации невозможно — вызывается error_policy::err(…)
В виду гибкой природы наборов параметров, лучшей политикой обработки ошибком может считаться “return false” (любое более строгое поведение будет на самом деле означать обязательность каждого параметра).
Тем ни менее, если зачем-то нужно, мы можем выбрать также из вот этих двух:
Дополнительным усовершенствованием может быть проверка избыточности для таких случаев как:
Мы не обсудили рантайм-техники, вроде:
Код работает на рантайме, пытаясь достать нужные ему параметры по ходу работы, соответственно мы имеем затраты времени, ну и об ошибке вы узнаете лишь когда она возникнет. Код далёк от идеала, я привожу его лишь как «proof of concept» и не думаю, что в таком виде его можно применять в реальных проектах.
А ещё я нашел предложение добавить именованные параметры в стандарт языка С++ вот здесь. Неплохо было бы.
Let's look at an example:
createArray(10, 20); // Что это значит? Что за "10" ? Что за "20" ?
createArray(length=10, capacity=20); // О, вот теперь понятнее!
createArray(capacity=20, length=10); // И наоборот тоже работает.
And another 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, when called, you need to change only 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 to emulate named parameters in C ++, well, and try to come up with something new.
Comments
Let's start with a fake, but simplest way - emulating named parameters through comments :)
Window window {
10, // xPosition
20, // yPosition
100, // width
50 // height
};
This approach is very popular among Windows developers, as examples on MSDN are often provided with such comments.
Idiom of a “named parameter”
The idea comes from the style of programming in Java: create a proxy class that will include all optional parameters in the form of methods. After that, we can use the chain of calls of 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 accepts 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 logic for setting parameters. But to assign simple values, the above style with public methods will do just fine.
In this approach:
- Required parameters are still positional (the call to the OpenFile constructor must be the first and this cannot be changed)
- Optional parameters must have copy (move) constructors
- You need to write an additional proxy class
The idiom of the “package of parameters”
The idea is similar to the previous one and is taken from Davide Di Gennaro's Advanced C ++ Metaprogramming - the technique of using proxy objects to set parameters through the assignment operator (=), as a result, we get the following syntactic sugar:
MyFunction(begin(v), end(v), where[logger=clog][comparator=greater()]);
Entities involved:
- logger and comparator are global constants. The assignment operator simply returns a wrapped copy of the assigned value
- where is a global constant of type "package of parameters". Its operator [] simply returns a new proxy object, which replaces one of its members with a new argument.
In characters:
where = {a, b, c }
where[logger = x] → { a,b,c }[ argument<0>(x) ] → {x,b,c}
Outline of implementation:
// argument
template
struct argument
{
T arg;
argument(const T& that)
: arg(that)
{
}
};
// void argument - just to use operator=
template
struct argument
{
argument(int = 0)
{
}
template
argument operator=(const T& that) const
{
return that;
}
argument operator=(std::ostream& that) const
{
return that;
}
};
// "пакет аргументов" (хранит значения)
template
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
argument_pack operator[](const argument<0, T>& x) const
{
return argument_pack(x.arg, second, third);
}
template
argument_pack operator[](const argument<1, T>& x) const
{
return argument_pack(first, x.arg, third);
}
template
argument_pack operator[](const argument<2, T>& x) const
{
return argument_pack(first, second, x.arg);
}
};
enum { LESS, LOGGER };
const argument comparator = 0;
const argument logger = 0;
typedef argument_pack, std::ostream> pack_t;
static const pack_t where(basic_comparator(), less(), std::cout);
For the full code, check out the original book.
Although the technique seems interesting, in practice it is difficult to make it reasonably comfortable and general. In the book, it was generally presented not by solving the problem we are considering, but by an example of a “chain” call to the operator [].
Tags
Andrzej Krzemieński published an interesting post “Intuitive Interface” , where he suggested 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 f{std::allocator_arg, a}; // a - аллокатор
std::unique_lock l{m, std::defer_lock}; // отложенный lock
Andrzej proposed a generalized approach:
// не настоящий STL
std::vector v1(std::with_size, 10, std::with_value, 6);
As you understand, you will need to create a number of overloaded functions, and also you cannot choose the order of parameters. The pluses include the lack of the need for copy / transfer constructors. Passing defaults also works without problems. From the article: “Tags are not an ideal solution because they clog namespaces with overloaded functions that are useful only in a few places where they are called.”
In addition, one of the readers suggested a good idea for another tag implementation:
std :: vector v1 (std :: with_size (10), std :: with_value (6));
Boost
Boost has a parameter library .
As you might expect, this is a fairly complete and practical implementation. Example:
// код класса
#include
#include
#include
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 ++
The latest C ++ language standards open up new doors. Let's see if we can apply any of them to solve our problem.
Lambdas
The chaining method is too verbose. I do not want to add a bunch of functions that return the object itself. How about determining the 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 classical idiom of a named parameter, in which you need to explicitly register all the "chain" functions. Another option is to make a constructor of the File class that accepts an object of type FileRecipe.
How to improve the readability of 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 admit the possibility of getting in runtime an error “a required parameter is missing” - you can use the optional type.
I recently tried to use this approach to configure tests and mocks. For example, I needed to create tests for a simple dice game. Configuration and tests used to look like this:
TEST_F(SomeDiceGameConfig, JustTwoTurnsGame)
{
GameConfiguration gameConfig { 5u, 6, 2u };
}
Using this approach, they can 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 a macro so as not to be repeated in each test with the same lambdas invoked:
TEST_F(SomeDiceGameConfig, JustTwoTurnsGame)
{
auto gameConfig = CREATE_CONFIG(
r.NumberOfDice = 5u;
r.MaxDiceValue = 6;
r.NumberOfTurns = 2u;
);
}
Using Variadic Templates
Variadic Templates introduced in C ++ 11 can improve the method described above. Let's remember the tags again. Tags can be a better approach than a lambda + parameter object, since we do not need to create another object, there are no problems with copy constructors, all parameters are processed in the same way (with lambdas we had to process the required parameters differently). But tags can be a good enough approach only if we had:
- Get by declaring only one overloaded constructor or function
- Get the opportunity to freely determine the order of parameters (pairs "tag-value")
- Have both required and optional parameters
Something like:
File f { _readonly, true, _path, "some path" };
or:
File f { by_name, Args&&... args) {}
My idea is this: I want to use Variadic Templates to give the user the ability to determine the order of the parameters and omit the optional parameters.
Imagine two constructors:
File(string path, bool readonly, bool createIfNotExist) {} // все параметры обязательны
template
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 through all the parameters in the set and call the first constructor with the corresponding set of parameters. Viewing parameters and generating code 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 just 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
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 passing arguments: with the proxy we get syntactic sugar (operator =), but now we need to store and pass values (not very good for non-movable / copied 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 proxies).
Here's 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
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
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 last constructors always call the “classic” constructor to do the real work.
The following piece of code shows how the 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:
- Mandatory and optional parameters are used uniformly.
- The order is not defined rigidly
- The tagged method has no disadvantages associated with passing parameters
- The method with the proxy is very obvious (due to the operator =)
Minuses:
- Errors at the compilation stage can be difficult to understand (static_assert may help in some cases)
- Available parameters should be documented.
- "Pollution" of the namespace with unnecessary functions / constructors
- The default values are always calculated.
- The tagged method is not perfect in terms of visibility (the tag and value follow a comma)
- The proxy method is not ideal in terms of passing parameters
Pay attention to the first problem: Clang is smart enough to report a problem very clearly. Imagine that I forgot about the required parameter with the window name, here is the compiler output:
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 exactly and where it was missed.
Minimalistic approach using std :: tuple
[this paragraph was written by Davide Di Gennaro]
We can use the tuple functionality (std :: tuple) to write a very compact and portable implementation of our task. We will rely on a few simple principles:
- The set of parameters will be a special tuple, where after each "type of tag" its value will go (that is, the type will be something like (std :: tuple
) - The standard language library already includes functions for transferring / concatenating objects and tuples, which guarantees performance and correctness
- We will use a macro to define global constants representing a tag.
- The syntax for creating the parameter set will look like (tag1 = value1) + (tag2 = value2) + ...
- The client will accept the parameter set as a reference to the template type, i.e.
template
void MyFunction ([whatever], T & parameter_pack) // or const T &, T &&, etc. - Inside the function call, the client will extract the necessary values from the set of parameters and use them somehow (well, for example, write them to local variables):
namespace tag
{
CREATE_TAG(age, int);
CREATE_TAG(name, std::string);
}
template
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);
}
Here's what the implementation of this idea might look like.
First macro:
#include
#include
template
struct parameter {};
#define CREATE_TAG(name, TYPE) \
\
struct name##_t \
{ \
std::tuple, TYPE> operator=(TYPE&& x) const \
{ return std::forward_as_tuple(parameter(), 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, int> operator=(int&& x) const
{
return std::forward_as_tuple(parameter(), x);
}
age_t(int) {}
};
const age_t age = 0;
Conceptually assignment
age = 18
Converts to something like:
make_tuple(parameter(), 18);
Please note that we wrote:
std::tuple, int> operator=(int&& x) const
We require r-value on the right. This is done for security: for the sake of increasing the readability of code 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 semantics of displacement:
The difference between
std::tuple, int> operator=(int&& x) const
{
return std::make_tuple(parameter(), x);
}
and
std::tuple, int> operator=(int&& x) const
{
return std::forward_as_tuple(parameter(), x);
}
very thin. In the latter case, std :: tuple <..., int &&> is returned, but since the function returns std :: tuple <..., int>, the move constructor std :: tuple is called.
As an alternative, we could write:
std::tuple, int> operator=(int&& x) const
{
return std::make_tuple(parameter(), std::move(x));
}
And now we will write a suitable concatenation operator for our tuples.
We implicitly agree that all tuples starting with parameter were created by our code, so without any explicit validation, we simply throw parameter away.
template
std::tuple, P1..., parameter, P2...>
operator+ (std::tuple, P1...>&& pack1, std::tuple, P2...>&& pack2)
{
return std::tuple_cat(pack1, pack2);
}
Very simple function: checks that both tuples are of the form
tuple, type, [maybe something else]>
and connects them.
And finally, we will write a function to extract an argument from a set. Please note that this function has transfer semantics (i.e., after calling it, the parameter will be extracted from the set).
template
bool extract_from_pack(TAG tag, T& var, std::tuple, P...>& pack);
It works as follows: if the set contains parameter, then the variable receives 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
bool extract_from_pack(TAG tag, T& var, std::tuple, 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 set of parameters is of the form tuple
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);
когда продолжение итерации невозможно — вызывается error_policy::err(…)
template
struct extractor
{
template
static bool extract(USERTAG tag, T& var, std::tuple, P...>&& pack)
{
return extract(tag, var, std::get(pack), std::move(pack));
}
template
static bool extract(USERTAG tag, T& var, parameter p0, std::tuple&& pack)
{
return extractor<(N+2 >= sizeof...(P)) ? size_t(-1) : N+2, ERR>::extract(tag, var, std::move(pack));
}
template
static bool extract(USERTAG tag, T& var, parameter, std::tuple&& pack)
{
var = std::move(std::get(pack));
return true;
}
};
template
struct extractor
{
template
static bool extract(TAG tag, T& var, std::tuple, P...>&& pack)
{ return ERR::err(tag); }
};
template
bool extract_from_pack(TAG tag, T& var, std::tuple, P...>& pack)
{
return extractor<0, ERR>::extract(tag, var, std::move(pack));
}
В виду гибкой природы наборов параметров, лучшей политикой обработки ошибком может считаться “return false” (любое более строгое поведение будет на самом деле означать обязательность каждого параметра).
struct soft_error
{
template
static bool err(T)
{
return false;
}
};
Тем ни менее, если зачем-то нужно, мы можем выбрать также из вот этих двух:
struct hard_error
{
template
static bool err(T); // обратите внимание, что static_assert(false) здесь не работает. Можете ли вы догадаться почему?
};
struct throw_exception
{
template
static bool err(T)
{
throw T();
return false;
}
};
Дополнительным усовершенствованием может быть проверка избыточности для таких случаев как:
(age=18)+(age=19)
Финальные заметки
Мы не обсудили рантайм-техники, вроде:
void MyFunction (option_parser& pack)
{
auto name = pack.require("name").as();
auto age = pack.optional("age", []{ return 10; }).as();
...
}
Код работает на рантайме, пытаясь достать нужные ему параметры по ходу работы, соответственно мы имеем затраты времени, ну и об ошибке вы узнаете лишь когда она возникнет. Код далёк от идеала, я привожу его лишь как «proof of concept» и не думаю, что в таком виде его можно применять в реальных проектах.
А ещё я нашел предложение добавить именованные параметры в стандарт языка С++ вот здесь. Неплохо было бы.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Нужны ли в С++ именованные параметры на уровне языка?
- 45.6%Конечно, удобно же!161
- 24.9%Ну, мне не особо важно, но пусть будут88
- 29.4%Хватит ломать язык и добавлять всякую чушь!104