Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No CTAD for lambdas & function objects #4

Open
alabuzhev opened this issue Oct 4, 2022 · 5 comments
Open

No CTAD for lambdas & function objects #4

alabuzhev opened this issue Oct 4, 2022 · 5 comments

Comments

@alabuzhev
Copy link

Compile the code:

    struct s1 { static int f(int) { return 0; } };
    struct s2 { int operator()(int) const { return 0; } } S2;
    auto l1 = [](int) { return 0; };

    std::function sf1 = &s1::f;      // works
    std::function sf2 = S2;          // works
    std::function sf3 = l1;          // works

    std23::function_ref f1 = &s1::f; // works
    std23::function_ref f2 = S2;     // doesn't work
    std23::function_ref f3 = l1;     // doesn't work

Expected

Successful compilation in all 3 cases, same as std::function above.

Actual

Only the first case works.

2nd:

error: class template argument deduction failed:
error: no matching function for call to 'function_ref(main()::s2&)'

3rd:

error: class template argument deduction failed:
error: no matching function for call to 'function_ref(main()::<lambda(int)>&)'

Compiler Explorer:

https://godbolt.org/z/rsc1ndjoh

Is it by design / overlook? As mentioned, std::function supports all these cases.

@zhihaoy
Copy link
Owner

zhihaoy commented Oct 4, 2022

It is temporarily by design. You may note that std::move_only_function supports no CTAD (other than moving itself). The reasons are

  1. move_only_function is meant to erase type from multiple objects. Therefore, it should declare a type; otherwise, we're reducing all callable objects to a type similar to the first one that initializes the wrapper.
  2. The top 3 places where type-erased callable wrappers appear are members, the value of key-value pairs in a container, and parameters. You cannot use CTAD in any of those.
  3. Unlike std::function, move_only_function can be ref-qualified, const-qualified, noexcept-qualified. It is unclear how CTAD would handle these. The design in my mind is that noexcept should be reflected in the deduced call signature; others should not. But as noted by Arthur O'Dwyer and myself, the trouble is that if two callables differ only in noexcept deduce to different erased-type, assigning one to the other will create a wrapper on top of the wrapper. It is worse in function_ref since there can be a dangling wrapper on top of a wrapper.

These issues apply to std::function_ref as well, plus that function_ref has the potential of introducing dangling reference in an initializing declaration -- unless the right-hand side is a function (pointer).

In contrast, the CTAD on std::function was somewhat experimental. It's not harmful because std::function is immune from 3), though.

My feeling is that, CTAD on function_ref (and move_only_function) need motivation. I want to see some context and motivating examples; the consistency argument (i.e., to be consistent with std::function) doesn't apply here.

@alabuzhev
Copy link
Author

Thanks for the explanation.

Some context & motivation: I noticed that quite often I try to write

auto handler1 = [&](){};
auto handler2 = [&](){};

auto handler = condition?
    handler1 :
    handler2;

// Call handler more than once

This obviously does not work, because handler1 and handler2 are unrelated and have no common type.

Type-erasing them with std::function helps:

auto handler = condition?
	std::function(handler1) :
	std::function(handler2);

but obviously has an extra cost.

function_ref feels like a drop-in replacement (and improvement) in this context, but without CTAD it is more verbose (especially with complex signatures):

auto handler = condition?
	std23::function_ref<void()>(handler1) :
	std23::function_ref<void()>(handler2);

@zhihaoy
Copy link
Owner

zhihaoy commented Oct 4, 2022

If it is okay to spell the type once, maybe

auto handler = [&] -> std23::function_ref<void()> {
    if (cond)
        return handler1;
    else
        return handler2;
}();

https://godbolt.org/z/3r5Pqsdnr

If you definitely want some form of deduction, a weird trick is

auto handler =
    cond ? std23::function_ref(
               std23::nontype<&decltype(handler1)::operator()>, handler1)
         : std23::function_ref(
               std23::nontype<&decltype(handler2)::operator()>, handler2);

https://godbolt.org/z/bxhv45q4h

The trick can be an approach if you ditch operator() altogether. You can write

struct Handler1 {
    void fn() {}
} handler1;
struct Handler2 {
    void fn() {}
} handler2;

auto handler =
    cond ? std23::function_ref(std23::nontype<&Handler1::fn>, handler1)
         : std23::function_ref(std23::nontype<&Handler2::fn>, handler2);

https://godbolt.org/z/EsenY6jaK

@alabuzhev
Copy link
Author

There are ways of course, but my goal was to reduce the typing and repeating myself, not to get some form of deduction just for the sake of it :)

The design in my mind is that noexcept should be reflected in the deduced call signature; others should not

Should it though? noexcept selling points are optimization opportunities and the contract, important in certain contexts, e.g. move ctors & operators. In case of function_ref optimizations are probably limited by the indirection anyway and I cannot immediately think of a good example where the noexcept-ness of it would affect the logic (except the infamous noexcept(noexcept(%the_whole_function_body%))). However, I am not an expert in this area.

the trouble is that if two callables differ only in noexcept deduce to different erased-type, assigning one to the other will create a wrapper on top of the wrapper

CTAD, just like auto, could deduce something completely different from the user's expectations in certain corner cases and cause troubles. However, I do not think it should be ditched completely because of that: as everything else, it is a tool that could make our lives easier in simple scenarios, should be used responsibly in complex ones and could always be ignored in favor of a more explicit way in case of any uncertainties.

@zhihaoy
Copy link
Owner

zhihaoy commented Oct 5, 2022

I can argue for noexcept, others can argue for const. Since our expectations are diverse, maybe the best approach is to have some traits that are used to fill signature types for all callable types. Like

static_assert(std::is_same_v<common_signature_t<std::function_ref, decltype(handler1), decltype(handler2)>,
                             std::function_ref<void()>);

and there can be common_signature_const_t, common_signature_nothrow_t, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants