How do we test if an expression of a certain type can be invoked with a prvalue?
Asked Answered
T

2

15

With we have fancy new is_invocable and fancy new prvalues that aren't really values.

This permits you to create an object without having to first logically construct it, then elide the construction.

I have run into a problem where using std::is_invocable to test if you can call something, and prvalue rules, seem to collide:

struct no_move {
  no_move(no_move&&)=delete;
  explicit no_move(int) {}
};
void f( no_move ) {}

now can we ask if f can be invoked using a prvalue of type no_move?

f( no_move(1) )

std::is_invocable< decltype(&f), no_move > doesn't work because it uses std::declval<no_move>() which is an xvalue like no_move&& not a prvalue of type no_move.

In this was the same, but guaranteed elision makes some functions callable with an xvalue (i.e., "T&&") and others with prvalues of type T.

Is there an alternative, or do we have to invent our own trait to handle this case?

(In a theoretical world where std::declval<T> returned T instead of T&&, is_invocable would, I believe, do the right thing).

Tambourine answered 9/1, 2018 at 18:32 Comment(4)
Maybe std::declval is now broken, and needs to return exactly what was passed in?Proboscis
@Proboscis std::declval2 maybe. Or std2::declval. But yes, a std::declval<T> that returned T would make std::is_invocable be able to answer the above question. That falls under "invent our own trait" at this point. I wonder if any existing c++14 code would break if declval<T>() returned T?Tambourine
@Yakk This is pretty strange, I don't know why they just didn't allow std::declval<T> to return exactly a T; if that were the case then you can still do declval<T&&> to do what current declval does, but by adding the reference for us it makes it impossible to "construct" the value type.Disapproval
@NirFriedman Requirements that T can be destroyed are then added to std::declval<T> based tests. So there is that.Tambourine
H
3

Is there an alternative, or do we have to invent our own trait to handle this case?

Yeah, you'd just have to write your own trait that doesn't use declval. Assuming you have std::is_detected lying around (which I know you certainly do):

template <typename T> T make();

template <typename F, typename... Args>
using invoke_result_t = decltype(std::declval<F>()(make<Args>()...));
//                               ^^^^^^^^^^^^^     ^^^^^

template <typename F, typename... Args>
using is_invocable = std::is_detected<invoke_result_t, F, Args...>;

This way, std::is_invocable<decltype(f), no_move> is false_type, but is_invocable<decltype(f), no_move)> is true_type.

I intentionally use declval<F>() for the function instead of make so as to allow using decltype(f) here. Really, invoke_result_t should be more complicated, and "do the right thing" for pointers to members, etc. But this is at least a simple approximation that indicates the viability of this approach.

Hypanthium answered 9/1, 2018 at 19:46 Comment(4)
It's not clear to me how useful this knowledge is, though.Phlebitis
@Phlebitis Maybe you have a function template which takes a callable, and the body calls it with a prvalue? It could happen.Hypanthium
@Barry: But the point behind "invocable" is that you're going to call std::invoke. That's why member pointers are invocable; because you can use std::invoke on them. You can't call them with ().Blamed
@NicolBolas That's because std::invoke() was the wrong way to solve the problem, and we should've made pointers-to-members invocable by actually making them invocable. I tried.Hypanthium
B
7

You are misusing the Invocable concept. This concept means nothing more than the ability to use std::invoke on the given function and the provided arguments.

You can't do std::invoke(f, no_move(1)), as this would provoke a copy/move of the forwarded argument. It is impossible for a prvalue to be used as a parameter through a forwarded call like invoke. You can pass a prvalue to the forwarding call, but the eventual call to the given function will get an xvalue.

This is a good reason to avoid using immobile types as value parameters in functions. Take them by const& instead.

C++ does not have a type trait to see if a function can be called with specific parameters in the way that you want.

Blamed answered 9/1, 2018 at 19:25 Comment(0)
H
3

Is there an alternative, or do we have to invent our own trait to handle this case?

Yeah, you'd just have to write your own trait that doesn't use declval. Assuming you have std::is_detected lying around (which I know you certainly do):

template <typename T> T make();

template <typename F, typename... Args>
using invoke_result_t = decltype(std::declval<F>()(make<Args>()...));
//                               ^^^^^^^^^^^^^     ^^^^^

template <typename F, typename... Args>
using is_invocable = std::is_detected<invoke_result_t, F, Args...>;

This way, std::is_invocable<decltype(f), no_move> is false_type, but is_invocable<decltype(f), no_move)> is true_type.

I intentionally use declval<F>() for the function instead of make so as to allow using decltype(f) here. Really, invoke_result_t should be more complicated, and "do the right thing" for pointers to members, etc. But this is at least a simple approximation that indicates the viability of this approach.

Hypanthium answered 9/1, 2018 at 19:46 Comment(4)
It's not clear to me how useful this knowledge is, though.Phlebitis
@Phlebitis Maybe you have a function template which takes a callable, and the body calls it with a prvalue? It could happen.Hypanthium
@Barry: But the point behind "invocable" is that you're going to call std::invoke. That's why member pointers are invocable; because you can use std::invoke on them. You can't call them with ().Blamed
@NicolBolas That's because std::invoke() was the wrong way to solve the problem, and we should've made pointers-to-members invocable by actually making them invocable. I tried.Hypanthium

© 2022 - 2024 — McMap. All rights reserved.