Apply std::common_type_t to deduce final return type in lambda return type deduction

560 views
Skip to first unread message

anonymous.fr...@gmail.com

unread,
Jun 23, 2018, 4:14:31 PM6/23/18
to ISO C++ Standard - Future Proposals
Problem:

When trying to mix std::optional with lambdas, I run into some rather annoying issue. I need to always use trailing return type syntax to get it deduced correctly in situations like this (e.g. when return std::nullopt comes first):

[](auto args)
{
   
if (!check(args))
       
return std::nullopt;

   
return foo(args); //returns some std::optional<T>
}

The above will not compile, as std::nullopt_t cannot be converted to std::optional<T>.

Context:

I'm trying to implement try_in_sequence, which basically executes functions passed as arguments one by one, until one succeeds (e.g. returns non-empty optional). Writing return type on every one of the 4-5 lambdas per function call is daunting. I understand that lambda's return type is consistent with auto, but it doesn't seem to be practical in this case.

Proposal:

Use std::common_type<return_stm_types...> as return type. E.g. use auto on each return statement individually, then deduce common type using `std::common_type`. std::common_type<std::nullopt_t, std::optional<int>> indeed yields std::optional<int>, as shown in shown in this example:


#include <type_traits>
#include <optional>

template <typename>
struct undefined;

int main()
{
   
using common_t = std::common_type_t<std::nullopt_t, std::optional<int>>;
   
static_assert(std::is_same_v<std::optional<int>, common_t>);
   
//undefined<std::common_type_t<std::nullopt_t, std::optional<int>>> foo;
}

Possible damage:

I cannot think of any at the moment. I never encountered a case where compilation error inside of lambda would be of any use. I believe this shouldn't affect existing code, as std::common_type is conversion rules friendly (not 100% sure though).





Thiago Macieira

unread,
Jun 23, 2018, 4:58:05 PM6/23/18
to std-pr...@isocpp.org
On Saturday, 23 June 2018 13:14:31 PDT anonymous.fr...@gmail.com
wrote:
> I cannot think of any at the moment. I never encountered a case where
> compilation error inside of lambda would be of any use. I believe this
> shouldn't affect existing code, as std::common_type is conversion rules
> friendly (not 100% sure though).

You're thinking of the fact that you'll only make invalid code valid. In that
case, sure, there's no issue, since your proposal cannot change valid code.

What you're missing is that the rules are like that for a reason. Deduction is
intentional that it requires all return statements to return the same thing,
to prevent one type from being accidentally converted to another or -- worse
-- changing the function's return type when adding or removing a return
statement after refactoring. Your proposal needs to address why the
intentional rule should be changed and how the protection that the rule
afforded can either be re-obtained or is something we can do away with.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center



anonymous.fr...@gmail.com

unread,
Jun 23, 2018, 5:10:48 PM6/23/18
to ISO C++ Standard - Future Proposals
On Sunday, June 24, 2018 at 2:58:05 AM UTC+6, Thiago Macieira wrote:
What you're missing is that the rules are like that for a reason. Deduction is
intentional that it requires all return statements to return the same thing,
to prevent one type from being accidentally converted to another or -- worse
-- changing the function's return type when adding or removing a return
statement after refactoring.


I believe that removing a return statement will actually be bug in itself. For example in case with std::optional, leaving out nullopt won't be an error and will be correct anyway. In case of leaving out non-empty return, the value will either implicitly convert or be an error, which should break the rest of the code (e.g. explode, making error visible). This is only my case though, so I'll try to search for some more usages of the lambdas in general, and parse standard to search for edge cases. I can only think of github to walk around, may be SO. Could you please suggest a place where I could reach out to people with something like "Will this break your code?" I'm thinking about implementing the feature and trying to compile and run some projects off Github. Thank you for feedback.
 

Nicol Bolas

unread,
Jun 23, 2018, 7:00:11 PM6/23/18
to ISO C++ Standard - Future Proposals, anonymous.fr...@gmail.com


On Saturday, June 23, 2018 at 4:14:31 PM UTC-4, anonymous.fr...@gmail.com wrote:
Problem:

When trying to mix std::optional with lambdas, I run into some rather annoying issue. I need to always use trailing return type syntax to get it deduced correctly in situations like this (e.g. when return std::nullopt comes first):

[](auto args)
{
   
if (!check(args))
       
return std::nullopt;

   
return foo(args); //returns some std::optional<T>
}

The above will not compile, as std::nullopt_t cannot be converted to std::optional<T>.

Well, the code could be rewritten as `return check(args) ? foo(args) : std::nullopt;`. Lambdas that are large and complicated enough that you can't use ?: for these cases are probably large and complicated enough that declaring a return type is not an onerous burden. Indeed, it would likely improve code clarity to do so.

Context:

I'm trying to implement try_in_sequence, which basically executes functions passed as arguments one by one, until one succeeds (e.g. returns non-empty optional). Writing return type on every one of the 4-5 lambdas per function call is daunting. I understand that lambda's return type is consistent with auto, but it doesn't seem to be practical in this case.

While I feel for you, P1012 (allowing fold expressions over the ?: operator) would be a much more reasonable solution to your problem than making such a drastic change. But in any case, making such a change for just this seems to be too narrow of an issue.
 

Proposal:

Use std::common_type<return_stm_types...> as return type. E.g. use auto on each return statement individually, then deduce common type using `std::common_type`. std::common_type<std::nullopt_t, std::optional<int>> indeed yields std::optional<int>, as shown in shown in this example:


#include <type_traits>
#include <optional>

template <typename>
struct undefined;

int main()
{
   
using common_t = std::common_type_t<std::nullopt_t, std::optional<int>>;
   
static_assert(std::is_same_v<std::optional<int>, common_t>);
   
//undefined<std::common_type_t<std::nullopt_t, std::optional<int>>> foo;
}

Possible damage:

I cannot think of any at the moment. I never encountered a case where compilation error inside of lambda would be of any use. I believe this shouldn't affect existing code, as std::common_type is conversion rules friendly (not 100% sure though).

The question is not one of whether it is possible. Yes, it's possible without breaking existing code.

It's whether it's a good idea. Doing this makes code potentially more difficult to read, since you can no longer look at a single return statement to know what type a function returns.

It also ignores the fact that the current rules actually catch bugs before they happen. By forcing users to make sure all return types are the same, you prevent someone from accidentally returning the wrong thing. There is nothing wrong with making the writer of a function sit down and work out exactly what it is they want to return in complicated cases. And the more return statements a function has, the more complicated it is.

Richard Smith

unread,
Jun 25, 2018, 1:24:25 PM6/25/18
to std-pr...@isocpp.org
The current rules permit a function to call itself recursively after its first return statement. How would you handle that?

common_type decays its arguments. How would you handle a return type of auto& or decltype(auto)?

This adds coupling between the language and library. What should happen if a declaration of common_type has not been included prior to the definition of a function with deduced return type?

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/7772686a-7e80-4c28-a87b-c5dc11450cc6%40isocpp.org.

Tony V E

unread,
Jun 25, 2018, 2:39:51 PM6/25/18
to Richard Smith
We could at least avoid common_type by instead talking about as if by ?:

Sent from my BlackBerry portable Babbage Device
From: Richard Smith
Sent: Monday, June 25, 2018 1:24 PM
Subject: Re: [std-proposals] Apply std::common_type_t to deduce final return type in lambda return type deduction

Richard Smith

unread,
Jun 25, 2018, 3:29:11 PM6/25/18
to std-pr...@isocpp.org
On 25 June 2018 at 11:39, Tony V E <tvan...@gmail.com> wrote:
We could at least avoid common_type by instead talking about as if by ?:

Yes; I wish the language rule had originally been defined that way. The self-recursive function case doesn't seem sufficiently compelling to me to take priority over supporting multiple returns with different types.

(Note that this would not permit common_type to be used as a customization point for return type deduction, but that seems OK to me... ?: doesn't support that either, and it doesn't seem to be a big deal in practice -- if we want to make that work, adding a way to overload operator?: enough to guide its type deduction -- rather than specializing common_type -- seems like a reasonable way forward:

template<typename R1, typename P1, typename R2, typename P2>
  operator?:(chrono::duration<R1, P1>, chrono::duration<R2, P2>) -> chrono::duration<common_type_t<R1, R2>, gcd<P1, P2>>;

... but I'm not suggesting we pursue that at this time.)

I suppose we could say that return type deduction happens at the end of the function or at the first recursive call, whichever comes first, and that the program is ill-formed if there's a recursive call and the return type that would be deduced at the end of the function differs from the return type deduced at the recursive call.
 
Sent from my BlackBerry portable Babbage Device
From: Richard Smith
Sent: Monday, June 25, 2018 1:24 PM
Subject: Re: [std-proposals] Apply std::common_type_t to deduce final return type in lambda return type deduction

To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

Nicol Bolas

unread,
Jun 25, 2018, 4:26:56 PM6/25/18
to ISO C++ Standard - Future Proposals
On Monday, June 25, 2018 at 3:29:11 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 11:39, Tony V E <tvan...@gmail.com> wrote:
We could at least avoid common_type by instead talking about as if by ?:

Yes; I wish the language rule had originally been defined that way.

... it is defined that way. `std::common_type` is defined in terms of the rules for `?:`, not the other way around.

The self-recursive function case doesn't seem sufficiently compelling to me to take priority over supporting multiple returns with different types.

But do we really want to support that? Or more to the point, do we really need to avoid writing a type name this badly?

Return type deduction is not free. You're taking an important piece of information which is well-specified in a specific location and obscuring it. Being able to inspect what a function does quickly is a good thing, and having to peer through its code to figure out what's going on is a bad thing.

So for me, in order for the gains of return type deduction to outweigh the drawbacks, one of the following must be true:

1. The return type is obvious based on the function's name and its parameters. A function named `add` that takes two parameters naturally returns whatever type is the sum of those parameters. So telling us what we can work out for ourselves is pointless redundancy.

2. The function is very short, 5 statements or less. Telling us what we can see from within the function quite easily is pointless redundancy.

3. The return type is difficult to type (large metaprogramming thing based on the types of parameters) or impossible to type (lambdas). Writing a big, long thing at the beginning (or end) of our function makes it difficult to read or understand.

If none of these are true, I would say that return type deduction makes code harder to deal with rather than easier. A function with multiple return statements almost certainly fails #2. #3 can still happen, but not in the case the OP outlined when dealing with a clear case of `optional<int>`.

So what we're left with is something that only rarely makes code easier to read. While simultaneously not catching mistakes where you happen to accidentally change the function's return type.

Overall, I just think that this is way too much of a corner case to put it into the standard. I hate to use principles as bludgeons for features I don't like, but Bjarne's "Remember the Vasa" post would seem to speak to this. It's not making the language easier to use; it's making the language more expert-friendly, at the detriment to ease of use.

To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

anonymous.fr...@gmail.com

unread,
Jun 25, 2018, 5:39:27 PM6/25/18
to ISO C++ Standard - Future Proposals
On Tuesday, June 26, 2018 at 2:26:56 AM UTC+6, Nicol Bolas wrote:
To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

I just thought that it would be easier than introducing some other construct. What I'd like is basically an equivalent to the matcher macro below:

#include <optional>
#define matcher [&]()->std::optional

#include <iostream>
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::optional<T>& val)
{
   if (val.has_value())
       os << *val;
   else
       os << "*null*";
   return os;
}

int main() {
   int target = 0;
   auto zero_matcher = matcher<int>{
       if (target == 0)
           return target;
       else
           return std::nullopt;
   };
   
   std::cout << zero_matcher() << '\n';
   target = 3;
   std::cout << zero_matcher() << '\n';
}

The problem I have with the solution is of course it being a macro. I thought if metaclasses can solve the problem, but after reading it through I couldn't confidently say yes or no. I believe the solution above, if had language support, would satisfy most of your points about clarify of code and not removing any existing bug-catcher functionality. I've never written ISO proposal in my life, so writing something completely new would be pretty hard for me.

anonymous.fr...@gmail.com

unread,
Jun 25, 2018, 6:04:06 PM6/25/18
to ISO C++ Standard - Future Proposals, anonymous.fr...@gmail.com
When I think of it now, it seems like some clojure-esque solution would be easier to introduce. The language supported alternative to matcher perhaps would be a better idea:
#include <optional>
#define matcher [&]()->std::optional

#include <iostream>
template <typename T>
std
::ostream& operator<<(std::ostream& os, const std::optional<T>& val)
{
   
if (val.has_value())
        os
<< *val;
   
else
        os
<< "*null*";
   
return os;
}

int main() {
   
int target = 0;
   
auto zero_matcher = matcher<int>{
       
if (target == 0)
           
return target;
       
else
           
return std::nullopt;
   
};
   
    std
::cout << zero_matcher() << '\n';
    target
= 3;
    std
::cout << zero_matcher() << '\n';
}

Now, the declaration probably should be like a template declaration, otherwise the only implementation would be virtual call style. Also, if it indeed looks like a template, it could integrate well with concepts and have already existing functionality to apply type erasure on them, like std::function and clojure shown on MeetingC++.

Richard Smith

unread,
Jun 25, 2018, 6:12:57 PM6/25/18
to std-pr...@isocpp.org
On 25 June 2018 at 13:26, Nicol Bolas <jmck...@gmail.com> wrote:
On Monday, June 25, 2018 at 3:29:11 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 11:39, Tony V E <tvan...@gmail.com> wrote:
We could at least avoid common_type by instead talking about as if by ?:

Yes; I wish the language rule had originally been defined that way.

... it is defined that way. `std::common_type` is defined in terms of the rules for `?:`, not the other way around.

Sorry, we have miscommunicated: "the language rule" I'm talking about is return type deduction, and how it handles the case of multiple types (not common_type). It (return type deduction) is not defined as using ?: to compute a common type, as you are no doubt aware.

The self-recursive function case doesn't seem sufficiently compelling to me to take priority over supporting multiple returns with different types.

But do we really want to support that? Or more to the point, do we really need to avoid writing a type name this badly?

Return type deduction is not free. You're taking an important piece of information which is well-specified in a specific location and obscuring it. Being able to inspect what a function does quickly is a good thing, and having to peer through its code to figure out what's going on is a bad thing.

So for me, in order for the gains of return type deduction to outweigh the drawbacks, one of the following must be true:

1. The return type is obvious based on the function's name and its parameters. A function named `add` that takes two parameters naturally returns whatever type is the sum of those parameters. So telling us what we can work out for ourselves is pointless redundancy.

2. The function is very short, 5 statements or less. Telling us what we can see from within the function quite easily is pointless redundancy.

3. The return type is difficult to type (large metaprogramming thing based on the types of parameters) or impossible to type (lambdas). Writing a big, long thing at the beginning (or end) of our function makes it difficult to read or understand.

If none of these are true, I would say that return type deduction makes code harder to deal with rather than easier. A function with multiple return statements almost certainly fails #2. #3 can still happen, but not in the case the OP outlined when dealing with a clear case of `optional<int>`.

So what we're left with is something that only rarely makes code easier to read. While simultaneously not catching mistakes where you happen to accidentally change the function's return type.

Overall, I just think that this is way too much of a corner case to put it into the standard. I hate to use principles as bludgeons for features I don't like, but Bjarne's "Remember the Vasa" post would seem to speak to this. It's not making the language easier to use; it's making the language more expert-friendly, at the detriment to ease of use.

To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

I think there are examples when it is worth the cost (particularly, the cases where the function has a primary return statement and some bailouts that return nullptr or nullopt or similar). Conversely, I'm not convinced that supporting return type deduction for (directly) recursive functions is worth the cost -- that seems like the expert-only feature in this context. Conversely, having a single general type unification algorithm that is used in all contexts where it makes sense makes the language simpler and more beginner-friendly (I seem to recall a good blog post on this subject -- perhaps from Eric Lippert -- a few years back, about how the C# language was simplified by using the same rules for ?: type unification and for type unification in return type deduction, but I can't find it right now...). A beginner expects to be able to use nullptr where they would have used a value of a pointer type, and saying you can't do that in a return statement in a function with a deduced return type is an unnecessary complication. ("Why can't the compiler just work it out for itself? The intended return type is obvious.")

Nevin Liber

unread,
Jun 25, 2018, 6:27:13 PM6/25/18
to std-pr...@isocpp.org
On Mon, Jun 25, 2018 at 5:12 PM Richard Smith <ric...@metafoo.co.uk> wrote:
I think there are examples when it is worth the cost (particularly, the cases where the function has a primary return statement and some bailouts that return nullptr or nullopt or similar).

The question shouldn't be "does it make a few specific situations better".  The question should be "does it make every situation the same or better, and if not, is it worth the cost?"

The current rule is very beginner-friendly:  if there are multiple returns, the types either have to match exactly or the return type has to be specified.

Having to understand [expr.cond] is not IMO beginner-friendly, even though it may make a few specific cases better.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com>  +1-847-691-1404

Nicol Bolas

unread,
Jun 25, 2018, 9:35:46 PM6/25/18
to ISO C++ Standard - Future Proposals, anonymous.fr...@gmail.com
On Monday, June 25, 2018 at 5:39:27 PM UTC-4, anonymous.fr...@gmail.com wrote:
On Tuesday, June 26, 2018 at 2:26:56 AM UTC+6, Nicol Bolas wrote:
To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

I just thought that it would be easier than introducing some other construct.

You misunderstand. My point was that ?: is not the most common expression in the world, so not everyone is 100% aware of all of its idiosyncrasies. Therefore, if they see a complex ?: expression where the two subexpressions are distinct types, they'd have to look up how that all works out. Which is fine; it's not a hugely common case, so having something to look up is fine.

By contrast, figuring out the return type of a function shouldn't be hard. You need to be able to do this to know how to use the function. If you make figuring out the return type as hard as ?: can sometimes be, then that's a problem.

What I'd like is basically an equivalent to the matcher macro below:

#include <optional>
#define matcher [&]()->std::optional

#include <iostream>
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::optional<T>& val)
{
   if (val.has_value())
       os << *val;
   else
       os << "*null*";
   return os;
}

int main() {
   int target = 0;
   auto zero_matcher = matcher<int>{
       if (target == 0)
           return target;
       else
           return std::nullopt;
   };
   
   std::cout << zero_matcher() << '\n';
   target = 3;
   std::cout << zero_matcher() << '\n';
}
 

What you're asking for wouldn't help. Why? Because you still have to say `std::optional<int>` somewhere in that function. Observe:

[&]()
{
 
if(target)
   
return target;
 
return nullopt;
}

Under your requested rules, that is just as much a compile error as the expression `target ? target : nullopt`. There is no common type between `int` and `nullopt`, so there's no way for this to work. What you want would only work if you do this:

[&]()
{
 
if(target)
   
return std::optional<int>(target);
 
return nullopt;
}

And if you have to name `std::optional<int>` anyway, what's the point? Why not make your code more readable by just putting that in the return type?

And lastly, I don't know why I would want to support `matcher` anyway. It's too special case to be generally useful. Ir's exactly the sort of thing a macro is for: some internal contraction that really shouldn't leak out into other code.

Nicol Bolas

unread,
Jun 25, 2018, 9:40:08 PM6/25/18
to ISO C++ Standard - Future Proposals
On Monday, June 25, 2018 at 6:12:57 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 13:26, Nicol Bolas <jmck...@gmail.com> wrote:
On Monday, June 25, 2018 at 3:29:11 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 11:39, Tony V E <tvan...@gmail.com> wrote:
We could at least avoid common_type by instead talking about as if by ?:

Yes; I wish the language rule had originally been defined that way.

... it is defined that way. `std::common_type` is defined in terms of the rules for `?:`, not the other way around.

Sorry, we have miscommunicated: "the language rule" I'm talking about is return type deduction, and how it handles the case of multiple types (not common_type). It (return type deduction) is not defined as using ?: to compute a common type, as you are no doubt aware.

The self-recursive function case doesn't seem sufficiently compelling to me to take priority over supporting multiple returns with different types.

But do we really want to support that? Or more to the point, do we really need to avoid writing a type name this badly?

Return type deduction is not free. You're taking an important piece of information which is well-specified in a specific location and obscuring it. Being able to inspect what a function does quickly is a good thing, and having to peer through its code to figure out what's going on is a bad thing.

So for me, in order for the gains of return type deduction to outweigh the drawbacks, one of the following must be true:

1. The return type is obvious based on the function's name and its parameters. A function named `add` that takes two parameters naturally returns whatever type is the sum of those parameters. So telling us what we can work out for ourselves is pointless redundancy.

2. The function is very short, 5 statements or less. Telling us what we can see from within the function quite easily is pointless redundancy.

3. The return type is difficult to type (large metaprogramming thing based on the types of parameters) or impossible to type (lambdas). Writing a big, long thing at the beginning (or end) of our function makes it difficult to read or understand.

If none of these are true, I would say that return type deduction makes code harder to deal with rather than easier. A function with multiple return statements almost certainly fails #2. #3 can still happen, but not in the case the OP outlined when dealing with a clear case of `optional<int>`.

So what we're left with is something that only rarely makes code easier to read. While simultaneously not catching mistakes where you happen to accidentally change the function's return type.

Overall, I just think that this is way too much of a corner case to put it into the standard. I hate to use principles as bludgeons for features I don't like, but Bjarne's "Remember the Vasa" post would seem to speak to this. It's not making the language easier to use; it's making the language more expert-friendly, at the detriment to ease of use.

To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

I think there are examples when it is worth the cost (particularly, the cases where the function has a primary return statement and some bailouts that return nullptr or nullopt or similar). Conversely, I'm not convinced that supporting return type deduction for (directly) recursive functions is worth the cost -- that seems like the expert-only feature in this context.

Perhaps, but we already support that. Features that already exist no longer need to justify themselves (unless you're debating removing them, which has to have a lot more justification due to being a breaking change). Proposed features need to justify themselves. And they need to make sure that code doesn't get worse.

Conversely, having a single general type unification algorithm that is used in all contexts where it makes sense makes the language simpler and more beginner-friendly (I seem to recall a good blog post on this subject -- perhaps from Eric Lippert -- a few years back, about how the C# language was simplified by using the same rules for ?: type unification and for type unification in return type deduction, but I can't find it right now...). A beginner expects to be able to use nullptr where they would have used a value of a pointer type, and saying you can't do that in a return statement in a function with a deduced return type is an unnecessary complication. ("Why can't the compiler just work it out for itself? The intended return type is obvious.")

But the intended return type is not "obvious", as evidenced by the fact that you need to create a bunch of special rules to decide what it should be. The expression alone is insufficient.

Yes, `return nullptr;` would represent that the function returns a pointer type. But... what pointer type? You don't know; you now have to go fishing around for another `return` statement to figure out what it actually returns. You don't even know if it's a raw pointer; it could be a `unique_ptr` or whatever. And if you don't know what pointer type a function returns, how can you use it?

Yes, you can argue that the existing recursion rules have the same problem. I'm not exactly happy with them either, but in their defense, recursion is a lot more rare than wanting to do the kind of stuff you're talking about. So the problem that recursive calls create (ie: having to look at another `return` statement) is not often encountered.

Code is read more often than it is written. So maybe let's not optimize for the latter when it makes the former really hard.

Richard Smith

unread,
Jun 26, 2018, 4:30:33 PM6/26/18
to std-pr...@isocpp.org
On Mon, 25 Jun 2018, 18:40 Nicol Bolas, <jmck...@gmail.com> wrote:
On Monday, June 25, 2018 at 6:12:57 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 13:26, Nicol Bolas <jmck...@gmail.com> wrote:
On Monday, June 25, 2018 at 3:29:11 PM UTC-4, Richard Smith wrote:
On 25 June 2018 at 11:39, Tony V E <tvan...@gmail.com> wrote:
We could at least avoid common_type by instead talking about as if by ?:

Yes; I wish the language rule had originally been defined that way.

... it is defined that way. `std::common_type` is defined in terms of the rules for `?:`, not the other way around.

Sorry, we have miscommunicated: "the language rule" I'm talking about is return type deduction, and how it handles the case of multiple types (not common_type). It (return type deduction) is not defined as using ?: to compute a common type, as you are no doubt aware.

The self-recursive function case doesn't seem sufficiently compelling to me to take priority over supporting multiple returns with different types.

But do we really want to support that? Or more to the point, do we really need to avoid writing a type name this badly?

Return type deduction is not free. You're taking an important piece of information which is well-specified in a specific location and obscuring it. Being able to inspect what a function does quickly is a good thing, and having to peer through its code to figure out what's going on is a bad thing.

So for me, in order for the gains of return type deduction to outweigh the drawbacks, one of the following must be true:

1. The return type is obvious based on the function's name and its parameters. A function named `add` that takes two parameters naturally returns whatever type is the sum of those parameters. So telling us what we can work out for ourselves is pointless redundancy.

2. The function is very short, 5 statements or less. Telling us what we can see from within the function quite easily is pointless redundancy.

3. The return type is difficult to type (large metaprogramming thing based on the types of parameters) or impossible to type (lambdas). Writing a big, long thing at the beginning (or end) of our function makes it difficult to read or understand.

If none of these are true, I would say that return type deduction makes code harder to deal with rather than easier. A function with multiple return statements almost certainly fails #2. #3 can still happen, but not in the case the OP outlined when dealing with a clear case of `optional<int>`.

So what we're left with is something that only rarely makes code easier to read. While simultaneously not catching mistakes where you happen to accidentally change the function's return type.

Overall, I just think that this is way too much of a corner case to put it into the standard. I hate to use principles as bludgeons for features I don't like, but Bjarne's "Remember the Vasa" post would seem to speak to this. It's not making the language easier to use; it's making the language more expert-friendly, at the detriment to ease of use.

To know what the return value of a function is, you have to look up the rules of ?:'s common typing. Is that something we need to do? Or rather, is the gain from this really worth the cost?

I think there are examples when it is worth the cost (particularly, the cases where the function has a primary return statement and some bailouts that return nullptr or nullopt or similar). Conversely, I'm not convinced that supporting return type deduction for (directly) recursive functions is worth the cost -- that seems like the expert-only feature in this context.

Perhaps, but we already support that. Features that already exist no longer need to justify themselves (unless you're debating removing them, which has to have a lot more justification due to being a breaking change). Proposed features need to justify themselves. And they need to make sure that code doesn't get worse.

Oh, I agree, and that's why I said that I wish the language rule had originally been defined this way, not that we should change it today. The latter is a far less appealing proposition.

Conversely, having a single general type unification algorithm that is used in all contexts where it makes sense makes the language simpler and more beginner-friendly (I seem to recall a good blog post on this subject -- perhaps from Eric Lippert -- a few years back, about how the C# language was simplified by using the same rules for ?: type unification and for type unification in return type deduction, but I can't find it right now...). A beginner expects to be able to use nullptr where they would have used a value of a pointer type, and saying you can't do that in a return statement in a function with a deduced return type is an unnecessary complication. ("Why can't the compiler just work it out for itself? The intended return type is obvious.")

But the intended return type is not "obvious", as evidenced by the fact that you need to create a bunch of special rules to decide what it should be. The expression alone is insufficient.

Yes, `return nullptr;` would represent that the function returns a pointer type. But... what pointer type? You don't know; you now have to go fishing around for another `return` statement to figure out what it actually returns. You don't even know if it's a raw pointer; it could be a `unique_ptr` or whatever. And if you don't know what pointer type a function returns, how can you use it?

Yes, you can argue that the existing recursion rules have the same problem. I'm not exactly happy with them either, but in their defense, recursion is a lot more rare than wanting to do the kind of stuff you're talking about. So the problem that recursive calls create (ie: having to look at another `return` statement) is not often encountered.

Code is read more often than it is written. So maybe let's not optimize for the latter when it makes the former really hard.

Well, that ("really hard") is subjective, and I don't think either of us has the evidence to conclude it or its negation. But I would readily admit that the possibility would exist for people to abuse this feature in a way that degrades readability. However, return type deduction itself (or indeed any type deduction) already takes information away from the reader, and therefore should only be used where that information is a distraction or an irrelevance rather than being useful; I don't see that this case would be different in character (though it might be different in degree).

Earlier, you suggested that someone wishing to call a function using return type deduction would need to read its implementation to understand how to call it. If they do, I would suggest that function is abusing return type deduction regardless of whether it uses the extension we're discussing. The whole point of the function abstraction is that you don't need to pierce the veil to use the functionality.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

anonymous.fr...@gmail.com

unread,
Jul 3, 2018, 12:39:03 PM7/3/18
to ISO C++ Standard - Future Proposals, anonymous.fr...@gmail.com
Sorry for bothering you with so badly thought out idea last time. I'm coming back with something that I believe is more grounded and should be more "practical" in terms of usage. It's first time for me to participate in such a discussion, but I hope this feature will be a big improvement.
Here are the points I tried to cover, some of them are incomplete:

  • Don't break existing code and don't silently change behavior (No: but close, requires reserving one new keyword. This is new functionality)
  • Empower users without being confusing, e.g. simplify the common case and allow people with different needs get the power (Yes)
  • Play well with language and standard library
  • Do not deviate from general C++ syntax and ideas (I believe yes)
I believe I know what I actually need: a way to capture and reuse return type and argument types of a function. The feature should be usable in many places, including existing and upcoming standard library functionality.

Definition:
Closure is a way to capture return type and argument types of a function. The closure then can be used to match the callable with the return type and arguments types, or used as alias to create a lambda with the properties specified by closure.

Closure declaration:
closure closure_name(optional-argument-list) optional-noexcept optional-mutable -> return_type;

Then, the closure can be used in multiple ways. First being to be named lambda kind, to imrpove readability and better interoperate with IDEs and tools, as they can give a hint to programmer in the right direction. Here is the username_validator, which can be used to adjust the set of allowed usernames:

closure username_validator(const std::string& username) -> bool;

std::copy_if(new_users.begin(), new_users.end(), back_inserter(saved_users), username_validator  {
    return std::all_of(username.begin(), username.end(), std::isalpha) //omitted cast to unsigned char for brevity
});

The closure instantiation above will create a lambda with the return type and arguments specified by clojure declaration. As implied by word instantiation, the clojure itself is not a type, but a mere set of constraints that will be applied on a type or be used to create a lambda.  To be usable in generic functions, there is template related feature:

template closure declaration:
template <template-parameter-list>
closure closure_name(optional-parameter-list) -> return_type;

The syntax could be changed to one in templates for lambdas proposal. Every template argument should be available from inside of the closure in case it is instantiated. This can be used in many places in standard library:

template <typename T>
closure unary_predicate(const T& value) -> bool;

Then, one can use it like this:

std::remove_if(source.begin(), source.end(), dest.begin(), unary_predicate<int> {return value % 2 == 0;} );

Template arguments are unnecessary if they cannot be deduced from arguments into the function. In case of omitting the brackets, closure will produce generic lambda (the one with templated operator()). To reiterate:

valid:
template <typename T>
closure identity(const T& value) -> T; //same as [](const T& value) {return value;}

invalid:
template <typename T, typename U>
closure map(const T& value) -> U; //U cannot be deduced

In case of using the brackets, the template will be instantiated and the resulting lambda will have definitive argument and return types. Sometimes there might be a need to create an alias to a closure, which is handled as well. Here is the syntax:

closure alias declaration:
using closure closure_name(optional-rename-sequence) = declared_closure;

The rename sequence lets people to redefine the names of the input parameters, which is a verbose future by design (user can mess up the order), and allows programmers to retrofit the initial names to their context. Lets create an alias to specialized unary_predicate, and name it username_validator:

using closure username_validator(username=value) = unary_predicate<std::string>;

Either all or no names should be renamed. The last missing feature from lambdas is a capture list. It is handled as trailing capture list:

closure instantiation
declared_closure_name[optional-capture-list](optional-rename-list) optional-noexcept optional-mutable { /*optional-code*/}

One can use it to create a unique ID generator for usernames:

closure id_generator(std::string_view username) -> std::size_t;

using id_storage = std::unordered_map<std::string_view, std::size_t>;
auto generator = id_generator[generated_ids = id_storage{}, counter = std::size_t(0)] mutable {
    auto& id = generated_ids[username];   
    if (id == 0)
        id = counter++;
    return id;
}

Closures cannot be used as template parameters, because there is one-to-many relationship between a type and set of closures it matches. Thus, I believe using the syntax below should be both reasonable and flexible.

Constraining template parameter
template <closure_name parameter_name, ...>
struct struct_name {};

I'm not sure about the syntax of the following feature. It allows users to create a variable with the same closure as the operand on the right side:

Closure constrained variable declaration and definition
closure_name auto variable_name = lvalue_or_rvalue;

I thought about instanceof, but it would require one more keyword to reserve. With the above, the feature can be used in template parameter list:

template <typename It, unary_predicate<It::value_type> Predicate> //omitted some stuff for brevity
It remove_if(It first, It last, Predicate pred);

It should be noted that there is a new template parameter silently added to the template. The following will match the closure specified above (for iterators referring to integers).

struct dummy {
    bool operator()(int ) { return true; }
};

Reflection
One could also use the feature to provide introspection for the callable. I've already  written a lot, but I believe that closure_name::return_type could be used to inspect return type, and closure_name::parameters could be a template parameter pack. The thing is that closure already defeats the need for the such reflection in the first place. All operations not covered by closures I can think of are already handled by existing functionality that operate on types.

How is it different from combination of Concepts and lambdas?
Not much, and it is by design. closure is a combination of lambda and SFINAE. The feature should simplify most of the common template metaprogramming regarding accepting callables. Also it should clarify most of the lambda usage.Since abbreviated template parameters didn't make it into concepts, this should be conservative replacement for the time being (and probably used most often, as ADL and free functions are mostly used inside templates). If the abbreviated parameters are getting into, then there is certainly gonna be an overlap.

I'm not certain if I can implement the feature to test for edge cases as I'm still an undergraduate, but if there is any chance of this proposal being good and making into standard, I'll try my best.

----------------------------------
I was not sure if I should post it as a new proposal or  post it here, so I decided to reply to you first.

Magnus Fromreide

unread,
Jul 3, 2018, 7:06:05 PM7/3/18
to std-pr...@isocpp.org, anonymous.fr...@gmail.com
On Tue, Jul 03, 2018 at 09:39:03AM -0700, anonymous.fr...@gmail.com wrote:
> Sorry for bothering you with so badly thought out idea last time. I'm
> coming back with something that I believe is more grounded and should be
> more "practical" in terms of usage. It's first time for me to participate
> in such a discussion, but I hope this feature will be a big improvement.
> Here are the points I tried to cover, some of them are incomplete:
>
>
> - Don't break existing code and don't silently change behavior (No: but
> close, requires reserving one new keyword. This is new functionality)
> - Empower users without being confusing, e.g. simplify the common case
> and allow people with different needs get the power (Yes)

You failed. I do get confused by the idea and fail to see it's obvious
greatness.

Please look at a number of other proposals, preferrably successful ones,
and you will see that they generally have a rationale up front that discuss
the problem they are trying to solve.

Why is this a great idea, or even useful?

> *Reflection*
> One could also use the feature to provide introspection for the callable.
> I've already written a lot, but I believe that closure_name::return_type
> could be used to inspect return type, and closure_name::parameters could be
> a template parameter pack. The thing is that closure already defeats the
> need for the such reflection in the first place. All operations not covered
> by closures I can think of are already handled by existing functionality
> that operate on types.
>
> *How is it different from combination of Concepts and lambdas?*
> Not much, and it is by design. closure is a combination of lambda and
> SFINAE. The feature should simplify most of the common template
> metaprogramming regarding accepting callables. Also it should clarify most
> of the lambda usage.Since abbreviated template parameters didn't make it
> into concepts, this should be conservative replacement for the time being
> (and probably used most often, as ADL and free functions are mostly used
> inside templates). If the abbreviated parameters are getting into, then
> there is certainly gonna be an overlap.

In what way does it differ?
What does it enable that isn't possible to do in the language as of today?
(or, what does it make a lot easier? (note that this hill is way steeper!))

> I'm not certain if I can implement the feature to test for edge cases as
> I'm still an undergraduate, but if there is any chance of this proposal
> being good and making into standard, I'll try my best.
>
> ----------------------------------
> I was not sure if I should post it as a new proposal or post it here, so I
> decided to reply to you first.

This is a new feature, completley unrelated to your former idea and so it
should have gotten it's own thread and its own Subject:.

/MF

Jake Arkinstall

unread,
Jul 3, 2018, 8:02:07 PM7/3/18
to std-pr...@isocpp.org
I like what you've put forward here. Capturing the argument and return type of a function and allowing it to be used in many places is, IMHO, a very nice idea. WRT your suggested implementation, omitting the parameters is a bad move. I'd go for:

closure username_validator(const std::string&) -> bool;

Which is almost equivalent to

using username_validator = std::function<bool(const std::string&)>;

But with the benefit of being able to implement it inline:

do_something(begin(usernames), end(usernames), username_validator(const std::string& username){ ... });

Or even as a class method:

class A{
    username_validator B(const std::string&) const noexcept;
};

Where the return type is implied by the closure and an inconsistent parameter list would be an error.

Without keeping the parameter names, you introduce possible scope issues (closures, by definition, may interact with their environment). Besides, if you want a more general closure you dont want that name to be fixed.

On that note, I don't quite think that closure is the right term here. Sure, it can be used for closures (in their traditional sense) but I think it has wider potential where the term wouldn't make sense.

As this is just a new syntax for more clearly specifying something that we already have, I'm not sure how much support it will gain. But I think it's elegant enough for some discussion - this current thread is not the place for it, but if you repost as a new thread it would be beneficial.

Nicol Bolas

unread,
Jul 3, 2018, 8:58:44 PM7/3/18
to ISO C++ Standard - Future Proposals
On Tuesday, July 3, 2018 at 8:02:07 PM UTC-4, Jake Arkinstall wrote:
As this is just a new syntax for more clearly specifying something that we already have, I'm not sure how much support it will gain. But I think it's elegant enough for some discussion - this current thread is not the place for it, but if you repost as a new thread it would be beneficial.

Before you go starting up a new thread, you should realize that what you're asking for is nothing more than a form of static reflection: the ability to introspect the current function's (or a given function's) parameter/return types. And we're already progressing on that front.
Reply all
Reply to author
Forward
0 new messages