-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Pointers to const fn:s #2238
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
Conversation
How C++ does this: using fnptr = auto (int) -> int;
constexpr auto const_hof(fnptr f) -> int { return f(1); }
constexpr auto const_fn(int) -> int { return 0; }
auto nonconst_fn(int) -> int { return 0; }
int main() {
auto a = const_hof(const_fn); // OK, "runtime" context
auto b = const_hof(nonconst_fn); // OK, "runtime" context
constexpr auto c = const_hof(const_fn); // OK, forced constexpr context
constexpr auto d = const_hof(nonconst_fn); // ERROR, call to non-constexpr function 'int nonconst_fn(int)'
// in constexpr expansion of 'const_hof(nonconst_fn)'
} , i.e. the constexprness of a fnptr is checked on use of const fn in a context requiring constant evaluation, not on const fn definition. In that context we know exactly what function that fnptr came from so we can determine its constexprness. |
@petrochenkov Thanks for writing that up =) What is your preferred solution of the two alternatives you list and why? |
Adopting the approach from the C++ example and not introducing const fn pointers. const fn foo(a: bool) -> u8 {
if a {
10 // constexpr branch
} else {
panic!() // non-constexpr branch, errors when evaluated in constexpr context
}
} , so fn pointers can be checked for constexprness "on use" as well. In addition, |
@petrochenkov I buy that, so I'll close this RFC (in short order) when I've written a replacement RFC |
@petrochenkov I disagree with non-CTFE logic in |
I guess I change my mind then - not closing after all. |
@eddyb |
If we allow calls to runtime functions then there isn't any point to |
If you allow annotating a function with const fn call_non_const(f: fn(bool)) {
f(false)
} The user of the library uses it as follows: fn f(b: bool) {
if b {
// do something non-const
}
}
call_non_const(f); This last call is const, as long as the author of the library doesn't change I would sympathize with completely relinquishing the But if we allow functions to be Another idea: We could have both. Allow the annotation with |
Right ;) This RFC begins from the assumption that
I like that, particularly the guarantee bit - but could the latter part increase compile times? There's a certain benefit to being able to assume that "yep, this function promised it was a |
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
Some may argue that we don't need HOFs that are `const fn`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function pointers require some additional checks in miri to ensure noone transmutes function pointers in "bad" ways. It's probably fine to convert a const fn(&u32)
to a const fn(Option<&u32>)
, but not the other way around. Additionally someone might convert from fn()
to const fn()
and try to call that.
While these things are "forbidden" at runtime (UB), it's not clear how they should be checked at compile-time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat related: you can use the target's call ABI (or will be able to, soon enough), and if they match up, you can reuse the bits - if register bank/memory indirection doesn't match at all, it's quite UB. But also I mostly care about checking calls, not casts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we simply forbid that conversion? Would an alternative be to have UB at compile time (is that possible?) if such a cast is done improperly?
The conversion is not preventable, you can always do *(&foo as *const Foo as *const Bar)
to hide any conversion. The final call can be checked, but doing that right will require some implementation effort.
I like the idea of just allowing calls to "not necessarily const" functions from "const" functions, and only giving a compile error if the function indeed turns out to have not been declared with "const". This would work for all kinds of "indirect" calls, both via function pointers and via non-const trait methods (where the implementer of the trait does provide a const implementation, assuming #2237 is accepted in some form). Also, this would work for both trait objects and trait bounds in generic functions. |
I also like that logic, because it feels like the const fn rules that we have now, just fully generalized. |
I'm torn. I see the appeal of this sort of "duck typing" approach, but it feels like it runs pretty counter to Rust's general approach. My main concern is that it complicates the meaning of semver around const fn. Consider: Consider: // In revision 1:
const fn foo(i: u32, _: fn() -> u32) -> u32 {
i
}
// In revision 2:
const fn foo(i: u32, a: fn() -> u32) -> u32 {
if i > 22 { a() } else { i }
} Revisions 1 and 2 are semver compatible for ordinary functions, but -- for const fn -- consumers could stop compiling, if they happened to be passing a non-const fn. That said, I do think that this is an interesting point in the space. It's not nearly as lassez faire as "get rid of |
One of the things I sort of regret in rust's design is that That makes me wary. After all, we expect it to be very common that people declare functions as not const but then add UPDATE: Oh, hmm, that breakage is only "unused unsafe lints". I was wondering about that -- is that the only source of problems? If so, that definitely affects my opinion. =) |
Since you can't put a fn bound on a generic type, I don't think either removing unsafe or adding const will have any breaking changes. Adding const has a definite chance of triggering new instances of the const_err lint.
They are compile time compatible, but at runtime they'll still behave differently. Replacing the body of a function with |
I am talking about compile time. I'm presuming that the runtime behavior is considered acceptable. It's always a fine line between a "bug fix" and a "breaking change". |
As with the #2237 RFC; I'll close this one for now and write up a more coherent and comprehensive proposal sometime later. |
Rendered.
You may now write: