Skip to content

Commit

Permalink
Emit anonymous functions as mutable lambdas
Browse files Browse the repository at this point in the history
This example now works:

    main: () = {
        v: std::vector = ( 1, 2, 3, 4, 5 );

        //  Definite last use of v => move-capture v into f's closure
        f := :() -> forward _ = v$;

        //  Now we can access the vector captured inside f()...
        f().push_back(6);
        for f() do(e) std::cout << e;       // prints 123456
    }
  • Loading branch information
hsutter committed Nov 21, 2023
1 parent 0b333f3 commit 4bd0c04
Show file tree
Hide file tree
Showing 17 changed files with 40 additions and 34 deletions.
8 changes: 4 additions & 4 deletions include/cpp2util.h
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,7 @@ auto is( X const& x ) -> bool {

// Values
//
inline constexpr auto is( auto const& x, auto const& value ) -> bool
inline constexpr auto is( auto const& x, auto&& value ) -> bool
{
// Value with customized operator_is case
if constexpr (requires{ x.op_is(value); }) {
Expand Down Expand Up @@ -1313,7 +1313,7 @@ auto is( std::variant<Ts...> const& x );
// is Value
//
template<typename... Ts>
constexpr auto is( std::variant<Ts...> const& x, auto const& value ) -> bool
constexpr auto is( std::variant<Ts...> const& x, auto&& value ) -> bool
{
// Predicate case
if constexpr (requires{ bool{ value(operator_as< 0>(x)) }; }) { if (x.index() == 0) return value(operator_as< 0>(x)); }
Expand Down Expand Up @@ -1494,7 +1494,7 @@ constexpr auto is( X const& x ) -> bool

// is Value
//
inline constexpr auto is( std::any const& x, auto const& value ) -> bool
inline constexpr auto is( std::any const& x, auto&& value ) -> bool
{
// Predicate case
if constexpr (requires{ bool{ value(x) }; }) {
Expand Down Expand Up @@ -1542,7 +1542,7 @@ constexpr auto is( std::optional<U> const& x ) -> bool
// is Value
//
template<typename T>
constexpr auto is( std::optional<T> const& x, auto const& value ) -> bool
constexpr auto is( std::optional<T> const& x, auto&& value ) -> bool
{
// Predicate case
if constexpr (requires{ bool{ value(x) }; }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
pure2-bugfix-for-non-local-function-expression.cpp2:5:34: error: lambda expression in an unevaluated operand
template<typename T> concept v = []() -> bool { return true; }();
template<typename T> concept v = []() mutable -> bool { return true; }();
^
pure2-bugfix-for-non-local-function-expression.cpp2:7:41: error: lambda expression in an unevaluated operand
using u = std::type_identity_t<decltype([]() -> void{})>;
using u = std::type_identity_t<decltype([]() mutable -> void{})>;
^
pure2-bugfix-for-non-local-function-expression.cpp2:9:47: error: lambda expression in an unevaluated operand
class t: public std::type_identity_t<decltype([]() -> void{})> {
class t: public std::type_identity_t<decltype([]() mutable -> void{})> {
^
3 errors generated.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ auto insert_at(cpp2::in<int> where, cpp2::in<int> val) -> void;
"hello", "2022"};

std::string y {"\n"};
auto callback {[_0 = (&y)](auto const& x) -> void { std::cout << x << *cpp2::assert_not_null(_0); }};
auto callback {[_0 = (&y)](auto const& x) mutable -> void { std::cout << x << *cpp2::assert_not_null(_0); }};

std::ranges::for_each(vec, callback);
y = "-ish\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
// Passing a function expression
std::ranges::for_each(
vec,
[](auto& x) -> void { x += "-ish"; }
[](auto& x) mutable -> void { x += "-ish"; }
);

// Initializing from a function expression
auto callback {[](auto& x) -> void { x += " maybe"; }};
auto callback {[](auto& x) mutable -> void { x += " maybe"; }};
std::ranges::for_each(
vec,
std::move(callback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@

auto y {"\n"};
std::ranges::for_each
(vec, [_0 = std::move(y)](auto const& x) -> void { std::cout << x << _0; });
(vec, [_0 = std::move(y)](auto const& x) mutable -> void { std::cout << x << _0; });

auto callback {[](auto& x) -> void { x += "-ish"; }};
auto callback {[](auto& x) mutable -> void { x += "-ish"; }};
std::ranges::for_each(vec, std::move(callback));

for ( auto const& str : vec )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
"hello", "2022"};

std::ranges::for_each
(vec, [](auto const& x) -> void { std::cout << x << "\n"; });
(vec, [](auto const& x) mutable -> void { std::cout << x << "\n"; });

auto callback {[](auto& x) -> void { x += "-ish"; }};
auto callback {[](auto& x) mutable -> void { x += "-ish"; }};
std::ranges::for_each(vec, std::move(callback));

for ( auto const& str : vec )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"hello", "2023"};

std::string y {"\n"};
std::ranges::for_each(vec, [_0 = (&y)](auto const& x) -> void {
std::ranges::for_each(vec, [_0 = (&y)](auto const& x) mutable -> void {
std::cout << CPP2_UFCS_0(c_str, (*cpp2::assert_not_null(_0))) << x << *cpp2::assert_not_null(_0); }
);

auto callback {[](auto& x) -> void { x += "-ish"; }};
auto callback {[](auto& x) mutable -> void { x += "-ish"; }};
std::ranges::for_each(vec, std::move(callback));

for ( auto const& str : vec )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@

auto y {"\n"};
std::ranges::for_each
(vec, [_0 = std::move(y)](auto const& x) -> void { std::cout << _0 << x << _0; });
(vec, [_0 = std::move(y)](auto const& x) mutable -> void { std::cout << _0 << x << _0; });

auto callback {[](auto& x) -> void { x += "-ish"; }};
auto callback {[](auto& x) mutable -> void { x += "-ish"; }};
std::ranges::for_each(vec, std::move(callback));

for ( auto const& str : vec )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ class t;
// Standalone Cpp1 repro: https://godbolt.org/z/dznnYTvc6

#line 5 "pure2-bugfix-for-non-local-function-expression.cpp2"
template<typename T> concept v = []() -> bool { return true; }();
template<typename T> concept v = []() mutable -> bool { return true; }();

using u = std::type_identity_t<decltype([]() -> void{})>;
using u = std::type_identity_t<decltype([]() mutable -> void{})>;

class t: public std::type_identity_t<decltype([]() -> void{})> {
class t: public std::type_identity_t<decltype([]() mutable -> void{})> {

};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ using g_ret = int;
[[nodiscard]] auto f() -> f_ret{
int ri {0};
#line 3 "pure2-look-up-parameter-across-unnamed-function.cpp2"
auto pred {[](auto const& e) -> auto { return e == 1; }};
auto pred {[](auto const& e) mutable -> auto { return e == 1; }};
ri = 42;
std::move(pred)(ri);
return std::move(ri); // "return;" is implicit"
Expand All @@ -43,7 +43,7 @@ using g_ret = int;
cpp2::deferred_init<int> ri;
#line 10 "pure2-look-up-parameter-across-unnamed-function.cpp2"
ri.construct(0);
auto pred {[](auto const& e) -> auto { return e == 1; }};
auto pred {[](auto const& e) mutable -> auto { return e == 1; }};
ri.value() = 42;
std::move(pred)(ri.value());
return std::move(ri.value());
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/test-results/pure2-more-wildcards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#line 1 "pure2-more-wildcards.cpp2"

#line 2 "pure2-more-wildcards.cpp2"
[[nodiscard]] auto less_than(auto const& value) -> auto { return [_0 = value](auto const& x) -> auto { return cpp2::cmp_less(x,_0); }; }
[[nodiscard]] auto less_than(auto const& value) -> auto { return [_0 = value](auto const& x) mutable -> auto { return cpp2::cmp_less(x,_0); }; }

[[nodiscard]] auto main() -> int
{
Expand Down
8 changes: 4 additions & 4 deletions regression-tests/test-results/pure2-print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ requires (true) inline CPP2_CONSTEXPR T outer::object_alias = 42;
cpp2::Default.expects(CPP2_UFCS_0(empty, m) == false || false, "message");
cpp2::Bounds.expects([_0 = 0, _1 = CPP2_UFCS_0(ssize, m), _2 = 100]{ return cpp2::cmp_less(_0,_1) && cpp2::cmp_less(_1,_2); }() && true != false, "");
#line 35 "pure2-print.cpp2"
auto a {[]() -> void{}};
auto b {[]() -> void{}};
auto c {[]() -> void{}};
auto a {[]() mutable -> void{}};
auto b {[]() mutable -> void{}};
auto c {[]() mutable -> void{}};

for( ; CPP2_UFCS_0(empty, s); a() ) {break; }

Expand All @@ -133,7 +133,7 @@ requires (true) inline CPP2_CONSTEXPR T outer::object_alias = 42;

cpp2::Default.expects(true, "");

return [_0 = (s + CPP2_ASSERT_IN_BOUNDS(m, 0))]() -> std::string { return _0; }();
return [_0 = (s + CPP2_ASSERT_IN_BOUNDS(m, 0))]() mutable -> std::string { return _0; }();
}

template<typename T> [[nodiscard]] auto outer::mytype::values([[maybe_unused]] T const& unnamed_param_2) const& -> values_ret{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ namespace N {
#line 35 "pure2-types-order-independence-and-nesting.cpp2"
auto X::exx(cpp2::in<int> count) const& -> void{
// Exercise '_' anonymous objects too while we're at it
cpp2::finally auto_37_9 {[&]() -> void { std::cout << "leaving call to 'why(" + cpp2::to_string(count) + ")'\n"; }};
cpp2::finally auto_37_9 {[&]() mutable -> void { std::cout << "leaving call to 'why(" + cpp2::to_string(count) + ")'\n"; }};
if (cpp2::cmp_less(count,5)) {
CPP2_UFCS(why, (*cpp2::assert_not_null(py)), count + 1);// use Y object from X
}
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/test-results/version
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

cppfront compiler v0.3.0 Build 8B20:1424
cppfront compiler v0.3.0 Build 8B21:1401
Copyright(c) Herb Sutter All rights reserved

SPDX-License-Identifier: CC-BY-NC-ND-4.0
Expand Down
2 changes: 1 addition & 1 deletion source/build.info
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"8B20:1424"
"8B21:1401"
6 changes: 3 additions & 3 deletions source/reflect.h
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ namespace meta {
CPP2_UFCS(push_back, generated_lines, std::vector<source_line>());
auto lines {&CPP2_UFCS_0(back, generated_lines)};

auto add_line {[&, _1 = lines](cpp2::in<std::string_view> s) -> void{
auto add_line {[&, _1 = lines](cpp2::in<std::string_view> s) mutable -> void{
static_cast<void>(CPP2_UFCS(emplace_back, (*cpp2::assert_not_null(_1)), s, source_line::category::cpp2));
}};
{
Expand Down Expand Up @@ -1531,7 +1531,7 @@ auto cpp2_enum(meta::type_declaration& t) -> void
{
// Let basic_enum do its thing, with an incrementing value generator
CPP2_UFCS(basic_enum, t,
[](std::string& value, cpp2::in<std::string> specified_value) -> void{
[](std::string& value, cpp2::in<std::string> specified_value) mutable -> void{
if (!(CPP2_UFCS_0(empty, specified_value))) {
value = specified_value;
}else {
Expand All @@ -1548,7 +1548,7 @@ auto flag_enum(meta::type_declaration& t) -> void
{
// Let basic_enum do its thing, with a power-of-two value generator
CPP2_UFCS(basic_enum, t,
[](std::string& value, cpp2::in<std::string> specified_value) -> void{
[](std::string& value, cpp2::in<std::string> specified_value) mutable -> void{
if (!(CPP2_UFCS_0(empty, specified_value))) {
value = specified_value;
}else {
Expand Down
6 changes: 6 additions & 0 deletions source/to_cpp1.h
Original file line number Diff line number Diff line change
Expand Up @@ -4421,6 +4421,12 @@ class cppfront
emit(*n.parameters);
}

// For an anonymous function, make the emitted lambda 'mutable'
if (!n.my_decl->has_name())
{
printer.print_cpp2( " mutable", n.position() );
}

// For now, adding implicit noexcept only for move/swap/dtor functions
if (
n.is_move()
Expand Down

22 comments on commit 4bd0c04

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't updated to use this commit, but I think it might break me.
Function expressions that capture are no longer const-callable.
Even if they don't take advantage of being mutable (https://cpp2.godbolt.org/z/a5KW7nM1r):

main: () = {
  i  := 0;
  (f := :() = { _ = i$; }) _ = f();
}
main.cpp2:3:21: error: no matching function for call to object of type 'const (lambda at main.cpp2:4:17)'
    3 |   static_cast<void>(f());
      |                     ^
main.cpp2:4:17: note: candidate function not viable: 'this' argument has type 'const (lambda at main.cpp2:4:17)', but method is not marked const
    4 | auto const& f = [_0 = std::move(i)]() mutable -> void{static_cast<void>(_0);};
      |                 ^
1 error generated.

Reasonable code that didn't care about (im)mutability will stop working.
Maybe the use case should be left to explicit this parameters (https://en.cppreference.com/w/cpp/compiler_support):

C++23 feature Paper(s) GCC Clang MSVC Apple Clang
Explicit object parameter (deducing this) P0847R7 18 19.32* (partial)*

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a long time, I had been wondering how you could conciliate
Cpp2 this parameter vs. Cpp1 explicit object parameter, specially for lambdas.

For lambdas, it turns out that in Cpp2, this is illegal.
In principle, but due to #281, a reference capture is emitted.
Otherwise, the only legal way to use this is in a capture.
That means this is free real state in a function expression.

So we can have

  • :() 0; work as before this commit,
  • :(this) 0; also work, and
  • :(inout this) 0; emit a mutable lambda.

And we can use this to refer to the explicit object parameter, if the compiler supports it, with these macros:

#if defined(__cpp_explicit_this_parameter)
    #define CPP2_THIS_PARAMETER(...) this __VA_ARGS__ cpp2_this
    #define CPP2_MUTABLE
    #define CPP2_THIS cpp2_this
#else
    #define CPP2_THIS_PARAMETER(...) /* empty */
    #define CPP2_MUTABLE mutable
    #define CPP2_THIS []() { static_assert(false, "Explicit object parameter not supported."); return cpp2::nonesuch; }()
#endif

We only diagnose the use of this, not the parameter, so we can lower the above as follows:

Cpp2 Cpp1 Cpp1 preprocessed (Clang 18, MSVC) Cpp1 preprocessed (Clang 12-17, GCC 10+)
:() 0; []() -> auto { return 0; } []() -> auto { return 0; } []() -> auto { return 0; }
:(this) 0; [](CPP2_THIS_PARAMETER(auto const&)) -> auto { return 0; } [](this auto const& cpp2_this) -> auto { return 0; } []() -> auto { return 0; }
:(this) this(); [](CPP2_THIS_PARAMETER(auto const&)) -> auto { return CPP2_THIS(); } [](this auto const& cpp2_this) -> auto { return cpp2_this(); } []() -> auto { return /* error lambda */(); }
:(inout this) 0; [](CPP2_THIS_PARAMETER(auto &)) CPP2_MUTABLE -> auto { return 0; } [](this auto & cpp2_this) -> auto { return 0; } []() mutable -> auto { return 0; }
:(inout this) this(); [](CPP2_THIS_PARAMETER(auto &)) CPP2_MUTABLE -> auto { return CPP2_THIS(); } [](this auto & cpp2_this) -> auto { return cpp2_this(); } []() mutable -> auto { return /* error lambda */(); }

The only thing we lose to Cpp1 explicit object parameter is the explicit naming.

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing we lose to Cpp1 explicit object parameter is the explicit naming.

And being able to specify the type of this.
You can lift that restriction for the function expression object parameter.

@JohelEGP
Copy link
Contributor

@JohelEGP JohelEGP commented on 4bd0c04 Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example that broke (from #797 (comment)):

The example comes from c++17 - C++ range-v3 library: 'take'-ing first 3 perfect numbers works and halts; 'take'-ing first 4 doesn't stop after 4 - Stack Overflow.
Here you can see it running: https://cpp2.godbolt.org/z/v5r7bPxY5.
The raw loop doesn't suffer from double predicate calls nor does it need cache_last.

main.cpp2:19:23: error: no matching function for call to object of type 'const (lambda at main.cpp2:25:15)'
   19 |           CPP2_UFCS_0(consume, i);
      |                       ^~~~~~~
raw.githubusercontent.com/hsutter/cppfront/main/include/cpp2util.h:796:16: note: expanded from macro 'CPP2_UFCS_0'
  796 |         return FUNCNAME(CPP2_FORWARD(obj)); \
      |                ^~~~~~~~
main.cpp2:19:11: note: in instantiation of function template specialization 'perfects::operator()(const (lambda at main.cpp2:25:15) &)::(anonymous class)::operator()<int &>' requested here
   19 |           CPP2_UFCS_0(consume, i);
      |           ^
raw.githubusercontent.com/hsutter/cppfront/main/include/cpp2util.h:791:38: note: expanded from macro 'CPP2_UFCS_0'
  791 | #define CPP2_UFCS_0(FUNCNAME,PARAM1) \
      |                                      ^
main.cpp2:25:14: note: in instantiation of function template specialization 'perfects::operator()<(lambda at main.cpp2:25:15)>' requested here
   25 |   perfects(4)([](auto const& i) mutable -> auto { return static_cast<void>(std::cout << (cpp2::to_string(i) + " ") << std::flush);  });
      |              ^
main.cpp2:25:15: note: candidate function template not viable: 'this' argument has type 'const (lambda at main.cpp2:25:15)', but method is not marked const
   25 |   perfects(4)([](auto const& i) mutable -> auto { return static_cast<void>(std::cout << (cpp2::to_string(i) + " ") << std::flush);  });
      |               ^
1 error generated.

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, we can emit function expressions without a this parameter and no captures as static.

C++23 feature Paper(s) GCC Clang MSVC Apple Clang
static operator() P1169R4 13 16

@JohelEGP
Copy link
Contributor

@JohelEGP JohelEGP commented on 4bd0c04 Nov 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always found it curious that "this is explicit" seems to not apply to function expressions.
Where does :() v$ sits in this regard?
I'd err on the safe side of Cpp1 semantics that there really is a closure object.
So with this commit, we're making the implicit this parameter default to inout,
which isn't consistent with the default of in parameters.
See also #30.

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4bd0c04#commitcomment-133345681 is the exception where "this is explicit" needn't apply.

@hsutter
Copy link
Owner Author

@hsutter hsutter commented on 4bd0c04 Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! While working on merging #506 I noticed that branch didn't have this commit, and then I saw this thread.

I've given this thought, and I think what you want is a C++17 constexpr lambda -- and we already have the spelling f:() == { /*body*/ } for named functions we want to be constexpr, and I should support that for lambdas as well since they're the same as any function except for the name, so :() == { /*body*/ } should work and I now think it's just an oversight that I didn't think of that case until your note.

Why not this? Briefly: Even though Cpp1 lambdas happen to be specified in terms of a rewrite to a functor, that's not inherently the only way to think about them... my conceptual model of captures is actually closer to them being additional local variables that are "static" (persistent across calls), and local variables are non-const by default. Note also that if Cpp2 did expose that as a (this) parameter, for consistency Cpp2 would then also need to support more things that don't currently work, such as this.obj syntax in the body to work as a synonym for captured obj.

Again, thanks!

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've given this thought, and I think what you want is a C++17 constexpr lambda -- and we already have the spelling f:() == { /*body*/ } for named functions we want to be constexpr, and I should support that for lambdas as well since they're the same as any function except for the name, so :() == { /*body*/ } should work and I now think it's just an oversight that I didn't think of that case until your note.

This is a really surprising interpretation,
since I'm all I'm talking about is the Cpp1 mutable and the Cpp2 this parameter of function expressions.
Function expression bodies are already implicitly constexpr thanks to the Cpp1 semantics (see also #714 (reply in thread)).

@hsutter
Copy link
Owner Author

@hsutter hsutter commented on 4bd0c04 Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[edited to expand discussion]

Yes, Cpp1 lambda functions are implicitly constexpr, if they happen to satisfy the constexpr requirements (which continue to be relaxed in each edition of the standard). As of C++23 and P2242, nearly all non-constexpr code is allowed as long as the function isn't actually invoked in a constexpr context, except static and thread_local which is fine. For example, in C++23 a constexpr lambda can have a std::set local variable, and I just checked Clang and GCC and they support that. So saying constexpr shouldn't be a significant restriction on lambdas.

My observation was mainly that:

  • constexpr implies "no side effects, the function's output depends on the arguments supplied to its parameters only," which I think is what we'd want a const lambda to mean because that implies no mutation of the stored state.
  • == is what Cpp2 uses to lower to Cpp1 constexpr functions anyway, and it should work consistently for an unnamed function too and emit constexpr on the generated Cpp1 lambda... and once I fix it so == does that, we have the "not mutable" syntax you were looking for.

So correcting the == inconsistency also seemed to solve the problem of not having a way to say "not mutable" at the same time. That was my thought anyway.


You mentioned above you had an example that didn't work with lowering to a mutable lambda. Can you confirm whether that example would work with a constexpr lambda? (Probably the simplest thing is to compile it with cppfront and just manually change mutable to constexpr in the .cpp and see what the Cpp1 compiler thinks?)

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my conceptual model of captures is actually closer to them being additional local variables that are "static" (persistent across calls), and local variables are non-const by default.

that == also gives the "not mutable" syntax

This makes more sense.

Note also that if Cpp2 did expose that as a (this) parameter, for consistency Cpp2 would then also need to support more things that don't currently work, such as this.obj syntax in the body to work as a synonym for captured obj.

That can be left to Cpp1 with 4bd0c04#commitcomment-133309652.
In fact, the only thing I use this for there is to call it.
Because the only way to reference a capture is with captured_expression$.

You mentioned above you had an example that didn't work with lowering to a mutable lambda. Can you confirm whether that example would work with a constexpr lambda?

That would be any capturing lambda called through a const variable (https://cpp2.godbolt.org/z/jWorf3Eq1):

main: (args) = {
  (f := :() args$)
    _ = f();
}
main.cpp2:3:23: error: no matching function for call to object of type 'const (lambda at main.cpp2:4:17)'
    3 |     static_cast<void>(f());
      |                       ^
main.cpp2:4:17: note: candidate function not viable: 'this' argument has type 'const (lambda at main.cpp2:4:17)', but method is not marked const
    4 | auto const& f = [_0 = args]() mutable -> auto { return _0; };
      |                 ^
1 error generated.

If using == removes the mutable, it would naturally go back to working.

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it doesn't work (https://cpp2.godbolt.org/z/EoY1n65TE):

example.cpp2:4:17: error: constexpr function's return type 'args_t' is not a literal type
    4 | auto const& f = [_0 = args]() constexpr -> auto { return _0; };
      |                 ^
raw.githubusercontent.com/hsutter/cppfront/main/include/cpp2util.h:1679:8: note: 'args_t' is not literal because it is not an aggregate and has no constexpr constructors other than copy or move constructors
 1679 | struct args_t : std::vector<std::string_view>
      |        ^
1 error generated.

@hsutter
Copy link
Owner Author

@hsutter hsutter commented on 4bd0c04 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main: (args) = {
(f := :() args$)
_ = f();
}

Right, :() args$ is still shorthand for :() -> _ = args$. All functions do default to non-constexpr.

But this now works with the commit I just pushed:

main: (args) = {
  (f := :() == args$)
    _ = f();
}

Thanks again for the feedback!

@JohelEGP
Copy link
Contributor

@JohelEGP JohelEGP commented on 4bd0c04 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hsutter
Copy link
Owner Author

@hsutter hsutter commented on 4bd0c04 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, MSVC doesn't support P2242 yet.

I think the right thing for now is to put a comment in the code that in the current design path we're trying out, constexpr is the eventual target for all == functions including anonymous functions (with the intent that it declare 'depends only on the arguments'), but until we want to take a dependency on C++23 (P2242) we will emit it as const/whitespace instead.

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clang does, and it still fails.
P2242 doesn't lift the restriction on constexpr functions returning literal types.
Neither does P2280, which GCC now implements.

@hsutter
Copy link
Owner Author

@hsutter hsutter commented on 4bd0c04 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks... so for now == is the way to say "always the same result when invoked with the same arguments" (i.e., not mutable) but I've commented out the constexpr part for the future. Fixed here: c80f12a

Is that better?

@JohelEGP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, that works.
Too bad about losing the terse syntax when Cpp1 mutable won't work.

@AbhinavK00
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this another place in the language where we are losing "constant by default"?
While I agree with Herb's mental model of captured variables being just static local variables and local variables are mutable by default so lambdas ought to be mutable as in a comment above-

my conceptual model of captures is actually closer to them being additional local variables that are "static" (persistent across calls), and local variables are non-const by default.

But the problem with lambdas is that you can pass a local variable as an in parameter and it works fine but you can't do the same with mutable lambdas that captures. #868
Maybe terse lambdas could be made constexpr and un-terse syntax could emit mutable lambdas. This would work but seem inconsistent, other solutions are welcome.

@SebastianTroy
Copy link

@SebastianTroy SebastianTroy commented on 4bd0c04 Dec 4, 2023 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JohelEGP
Copy link
Contributor

@JohelEGP JohelEGP commented on 4bd0c04 Dec 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I only seem to interact with this project via email, does this mess with the experience of people who use GitHub directly?

Sometimes, the tail of a reply includes a copy of the whole discussion, which makes scrolling a tough job.
Maybe you can configure your email client to snip that when replying to GitHub.

@gregmarr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I only seem to interact with this project via email, does this mess with the experience of people who use GitHub directly?

I've found that people often edit their comments quite significantly, and GitHub doesn't email you when that happens.

Please sign in to comment.