Nikolay Ivchenkov
unread,Apr 24, 2013, 4:29:29 AM4/24/13Sign in to reply to author
Sign in to forward
You do not have permission to delete messages in this group
Either email addresses are anonymous for this group or you need the view member email addresses permission to view the original message
to std-pr...@isocpp.org
It's good to see that we have an official proposal about forwarding
lambdas. I was going to write similar paper, but now we can just
discuss this one.
> When dealing with generic algorithms, like the function templates in
> <algorithm>, it can be quite cumbersome to pass an overloaded
> function or a function template
... or a function with default arguments:
std::wstring to_wstring(
std::string const &s,
encoding_t encoding = default_encoding());
std::vector<std::string> src = source();
std::vector<std::wstring> dst;
std::transform(
src.begin(),
src.end(),
std::back_inserter(dst),
to_wstring); // won't work
In some cases we can't safely use address of a member function (even
with further explicit conversions), because the function's type is not
fixed: according to C++11 - 17.6.5.5,
An implementation may declare additional non-virtual member
function signatures within a class:
— by adding arguments with default values to a member function
signature; [Footnote: Hence, the address of a member
function of a class in the C++ standard library has an
unspecified type.] [ Note: An implementation may not add
arguments with default values to virtual, global, or
non-member functions.—end note ]
— by replacing a member function signature with default values
by two or more member function signatures with equivalent
behavior; and
— by adding a member function signature for a member function
name.
In general, we can take address of only those functions which are
guarateed to have the same type (and also be non-overloaded
non-templates if we don't use further casts) forever, but such promise
may be too strong in many cases, so we need something more flexible
than the built-in address-of operator. Short forwarding lambdas would
be very helpful.
> [](auto&&... vs)
> { return id-expression(std::forward<decltype(vs)>(vs)...); }
>
> The above allows the user to pass any overloaded function or
> function template as if it were a function object, bypassing the
> main issues mentioned in the motivation. However, there are
> drawbacks:
>
> * Writing out the whole lambda expression is tedious and introduces
> unnecessary clutter.
We could have more short notation:
forwarding-lambda-expression:
lambda-introducer lambda-declarator opt =>
assignment-expression
assignment-expression:
forwarding-lambda-expression
....
[](auto&&... vs) => id-expression(FORWARD(vs)...);
but
[] id-expression
is obviously more concise.
> * It only allows exactly that invocation form and dismisses member
> functions and member data.
I consider that as an advantage. If we want to use a class member, we
should express our intent explicitly. Moreover, an access through .
(dot) should be distinguishable from an access through -> (arrow).
For example,
x.reset()
and
x->reset()
may both be well-formed but have different meaning.
> * Operator support is incomplete, only allowing the non-member
> direct invocation syntax operator@(arguments...), ignoring member
> operator overloads as well as the special rules for operator
> function name look-up
I don't see problem here. We are free to change lambda definition
accordingly.
[](auto &&x) => @FORWARD(x)
[](auto &&x) => FORWARD(x)@
[](auto &&x, auto &&y) => (FORWARD(x) @ FORWARD(y))
> These drawbacks, especially the third one, can not be solved in a
> "library"-way (say, with a macro).
That's a controversial opinion. We could use several macro
definitions:
#define FORWARD(x) static_cast<decltype(x) &&>(x)
#define FUNC(f) \
[](auto &&... params) => f(FORWARD(params)...)
#define MEM_FUNC(f) \
[](auto &&obj, auto &&... params) => \
FORWARD(obj).f(FORWARD(params)...)
#define INDIRECT_MEM_FUNC(f) \
[](auto &&obj, auto &&... params) => \
FORWARD(obj)->f(FORWARD(params)...)
#define UNARY_OP(op) \
[](auto &&x) => op FORWARD(x)
#define POSTFIX_OP(op) \
[](auto &&x) => FORWARD(x) op
#define BINARY_OP(op) \
[](auto &&x, auto &&y) => (FORWARD(x) op FORWARD(y))
#define SUBSCRIPT_OP \
[](auto &&x, auto &&y) => FORWARD(x)[FORWARD(y)]
#define CALL_OP \
[](auto &&obj, auto &&... params) => \
FORWARD(obj)(FORWARD(params)...)
I would still prefer core language solution though :-)
> The semantics of INVOKE seemed like a good starting point
From my point of view, contrived uniformity is an evil. I can assume
that fast-to-write-but-hard-to-read programs are what some people
really want, but I don't fall into this category. If we need to waste
a lot of time in order to figure out what exactly some simple code is
supposed to do, because we have to thoroughly investigate large context
and apply several disambiguation rules, then visual simplicity and
uniformity don't look so cute.
I would prefer to have different syntax for different use cases,
rather than to deal with dirty tricks like INVOKE:
1) []id_expression
[](auto &&... params) => id_expression(FORWARD(params)...)
2) [].id_expression
[](auto &&x, auto &&... params) =>
FORWARD(x).id_expression(FORWARD(params)...)
3) []->id_expression
[](auto &&x, auto &&... params) =>
FORWARD(x)->id_expression(FORWARD(params)...)
4) [] = .id_expression
[](auto &&x) => FORWARD(x).id_expression
5) [] = ->id_expression
[](auto &&x) => FORWARD(x)->id_expression
Advantages:
* this approach is less error-prone, because we explicitly state what
we want exactly, while implicit assumptions made by a too smart
compiler have more chances to be wrong (i.e. not match original
intent)
auto m = [] = .run;
auto c = [].run;
struct F
{
std::function<void()> run;
} f;
m(f) = []{ /*...*/ }; // assigns []{ /*...*/ } to f.run
c(f); // calls f.run()
* the semantics is transparent for readers;
* simple correspondence would (potentially) help compilers to provide
better diagnostic messages, because if our code is ill-formed, we
don't need (potentially long) explanation why 5 different
interpretations are wrong / ill-formed.
> An idea was to allow []obj.id-expression for this-binding (like
> std::bind) - []x.foo would then create a nullary lifting-lamba.
I think, it should be [=]x.foo (x is captured by copy) or [&]x.foo
(x is captured by reference).