Sub-tracking issue for https://github.com/rust-lang/rust/issues/57563.
This tracks const fn types and calling fn types in const fn.
From the RFC (https://github.com/oli-obk/rfcs/blob/const_generic_const_fn_bounds/text/0000-const-generic-const-fn-bounds.md#const-function-pointers):
const function pointersconst fn foo(f: fn() -> i32) -> i32 {
f()
}
is illegal before and with this RFC. While we can change the language to allow this feature, two
questions make themselves known:
fn pointers in constants
const F: fn() -> i32 = ...;
is already legal in Rust today, even though the F doesn't need to be a const function.
Opt out bounds might seem unintuitive?
const fn foo(f: ?const fn() -> i32) -> i32 {
// not allowed to call `f` here, because we can't guarantee that it points to a `const fn`
}
const fn foo(f: fn() -> i32) -> i32 {
f()
}
Alternatively one can prefix function pointers to const functions with const:
const fn foo(f: const fn() -> i32) -> i32 {
f()
}
const fn bar(f: fn() -> i32) -> i32 {
f() // ERROR
}
This opens up the curious situation of const function pointers in non-const functions:
fn foo(f: const fn() -> i32) -> i32 {
f()
}
Which is useless except for ensuring some sense of "purity" of the function pointer ensuring that
subsequent calls will only modify global state if passed in via arguments.
I think that, at the very least, this should work:
const fn foo() {}
const FOO: const fn() = foo;
const fn bar() { FOO() }
const fn baz(x: const fn()) { x() }
const fn bazz() { baz(FOO) }
For this to work:
const must be part of fn types (just like unsafe, the extern "ABI", etc.)const fn types from const fnCurrently, const fns already coerce to fns, so const fn types should too:
const fn foo() {}
let x: const fn() = foo;
let y: fn() = x; // OK: const fn => fn coercion
I don't see any problems with supporting this. The RFC mentions some issues, but I don't see anything against just supporting this restricted subset.
This subset would be super useful. For example, you could do:
struct Foo<T>(T);
trait Bar { const F: const fn(Self) -> Self; }
impl<T: Bar> Foo<T> {
const fn new(x: T) -> Self { Foo(<T as Bar>::F(x)) }
}
const fn map_i32(x: i32) -> i32 { x * 2 }
impl Bar for i32 { const F: const fn(Self) -> Self = map_i32; }
const fn map_u32(x: i32) -> i32 { x * 3 }
impl Bar for u32 { const F: const fn(Self) -> Self = map_u32; }
which is a quite awesome work around for the lack of const trait methods, but much simpler since dynamic dispatch isn't an issue, as opposed to:
trait Bar { const fn map(self) -> Self; }
impl Bar for i32 { ... }
impl Bar for u32 { ... }
// or const impl Bar for i32 { ... {
This is also a way to avoid having to use if/match etc. in const fns, since you can create a trait with a const, and just dispatch on it to achieve "conditional" control-flow at least at compile-time.
AFAIK const fn types are not even RFC'd, isn't it too early for a tracking issue?
Don't know, @centril suggested that I open one.
I have no idea why const fn types aren't allowed. AFAICT, whether a function is const or not is part of its type, and the fact that const fn is rejected in a type is an implementation / original RFC oversight. If this isn't the case, what is the case?
EDIT: If I call a non-const fn from a const fn, that code fails to type check, so for that to happen const must be part of a fn type.
AFAIK
const fntypes are not even RFC'd, isn't it too early for a tracking issue?
Lotsa things aren't RFCed with respect to const fn. I want these issues for targeted discussion so it doesn't happen on the meta issue.
If I call a non-const fn from a const fn, that code fails to type check, so for that to happen const must be part of a fn type.
it is.
you can do
const fn f() {}
let x = f;
x();
inside a constant. But this information is lost when casting to a function pointer. Function pointers just don't have the concept of of constness.
Figuring out constness in function pointers or dyn traits is a tricky questions, with a lot of prior discussion in the RFC and the pre-RFC.
whats about?
pub trait Reflector {
fn Reflect(&mut self)-> (const fn(Cow<str>)->Option<Descriptor>);
}
I was fiddling with something while reading some of the discussion around adding a lazy_static equivalent to std and found that this check forbids even storing a fn pointer in a value returned from a const fn which seems unnecessarily restrictive given that storing them in const already works. The standard lazy types RFC winds up defining Lazy like:
pub struct Lazy<T, F = fn() -> T> { ... }
Here's a simple (but not very useful) example that hits this.
Adding another type parameter for the function makes it work on stable but it feels unnecessary.
Could this specific case be allowed without stabilizing the entire ball of wax here? (Specifically: referencing and storing fn pointers in const fn but not calling them.)
Could this specific case be allowed without stabilizing the entire ball of wax here? (Specifically: referencing and storing fn pointers in const fn but not calling them.)
The reason we can't do this is that this would mean we'd lock ourselves into the syntax that fn() means a not callable function pointer (which I do realize constants already do) instead of unifying the syntax with trait objects and trait bounds as shown in the main post of this issue
Just in case other people run into this being unstable: It's still possible to use function pointers in const fn as long as they're wrapped in some other type (eg. a #[repr(transparent)] newtype or an Option<fn()>):
#[repr(transparent)]
struct Wrap<T>(T);
extern "C" fn my_fn() {}
const FN: Wrap<extern "C" fn()> = Wrap(my_fn);
struct Struct {
fnptr: Wrap<extern "C" fn()>,
}
const fn still_const() -> Struct {
Struct {
fnptr: FN,
}
}
If const is a qualifier of function like unsafe or extern we have next issue:
const fn foo() { }
fn bar() { }
fn main() {
let x = if true { foo } else { bar };
}
It compiles now but not compiles with these changes because if and else have incompatible types.
So it breaks an old code.
If const is a qualifier of function like
unsafeorexternwe have next issue:const fn foo() { } fn bar() { } fn main() { let x = if true { foo } else { bar }; }It compiles now but not compiles with these changes because
ifandelsehave incompatible types.
So it breaks an old code.
Const fn's are still fns, the type won't be charged. In fact const qualifier only allows const fn appear in const contexes, and be evaluated at compile time. You can think of this like implicit casting.
unsafe fn foo() { }
fn bar() { }
fn main() {
let x = if true { foo } else { bar };
}
This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?
unsafe fn foo() { } fn bar() { } fn main() { let x = if true { foo } else { bar }; }This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?
This leads to possible unsafety in our code, and all which comes with it. const fn casting on otherside don't brings any unsafety, so is allowed, const fn must not have any side effects only, it is compatible with fn contract.
fn bar() {}
const fn foo(){}
const fn foo_bar(){
if true { foo() } else { bar() };
}
This must not compile, because bar is not const and therefore can't be evaluated at compile time. Btw, it raises(?) "can't call non const fn inside of const one", and can be considered incorrect downcasting. (set of valid const fns is smaller than set of fns at all)
fn main() {
let x = if true { foo } else { bar };
}
```This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?
This leads to possible unsafety in our code, and all which comes with it.
constfn casting on otherside don't brings any unsafety, so is allowed, const fn must not have any side effects only, it is compatible with fn contract.
I think you're missing the point. With implicit coercions, the type of x would be unsafe fn(), not fn(). There's nothing about that which leads to possible unsafety. Generally, const fn() can be coerced to fn() and fn() can be coerced to unsafe fn(). It just doesn't happen automatically, which is why changing const fn foo() to coerce into const fn() rather than fn() implicitly is a breaking change.
fn bar() {} const fn foo(){} const fn foo_bar(){ if true { foo() } else { bar() }; }This must not compile, because bar is not const and therefore can't be evaluated at compile time. Btw, it raises(?) "can't call non const fn inside of const one", and can be considered incorrect downcasting. (set of valid const fns is smaller than set of fns at all)
Of course this must not compile, but I don't think that's related to what @filtsin was talking about.
Personally I would love to see more implicit coercions for function pointers. Not sure how feasible that is though. I've previously wanted trait implementations for function pointer types to be considered when passing a function (which has a unique type) to a higher-order generic function. I posted about it on internals, but it didn't receive much attention.
Generally,
const fn()can be coerced tofn()andfn()can be coerced tounsafe fn(). It just doesn't happen automatically, which is why changingconst fn foo()to coerce intoconst fn()rather thanfn()implicitly is a breaking change.But not in oposite direction - thats what i wanted to say.
Most helpful comment
I was fiddling with something while reading some of the discussion around adding a
lazy_staticequivalent to std and found that this check forbids even storing afnpointer in a value returned from aconst fnwhich seems unnecessarily restrictive given that storing them inconstalready works. The standard lazy types RFC winds up definingLazylike:Here's a simple (but not very useful) example that hits this.
Adding another type parameter for the function makes it work on stable but it feels unnecessary.
Could this specific case be allowed without stabilizing the entire ball of wax here? (Specifically: referencing and storing
fnpointers inconst fnbut not calling them.)