Byrefs
F# has two major feature areas that deal in the space of low-level programming:
- The
byref
/inref
/outref
types, which are managed pointers. They have restrictions on usage so that you cannot compile a program that is invalid at run time. - A
byref
-like struct, which is a struct that has similar semantics and the same compile-time restrictions asbyref<'T>
. One example is Span<T>.
// Byref types as parameters
let f (x: byref<'T>) = ()
let g (x: inref<'T>) = ()
let h (x: outref<'T>) = ()
// Calling a function with a byref parameter
let mutable x = 3
f &x
// Declaring a byref-like struct
open System.Runtime.CompilerServices
[<Struct; IsByRefLike>]
type S(count1: int, count2: int) =
member x.Count1 = count1
member x.Count2 = count2
There are three forms of byref
:
inref<'T>
, a managed pointer for reading the underlying value.outref<'T>
, a managed pointer for writing to the underlying value.byref<'T>
, a managed pointer for reading and writing the underlying value.
A byref<'T>
can be passed where an inref<'T>
is expected. Similarly, a byref<'T>
can be passed where an outref<'T>
is expected.
To use a inref<'T>
, you need to get a pointer value with &
:
open System
let f (dt: inref<DateTime>) =
printfn $"Now: %O{dt}"
let usage =
let dt = DateTime.Now
f &dt // Pass a pointer to 'dt'
To write to the pointer by using an outref<'T>
or byref<'T>
, you must also make the value you grab a pointer to mutable
.
open System
let f (dt: byref<DateTime>) =
printfn $"Now: %O{dt}"
dt <- DateTime.Now
// Make 'dt' mutable
let mutable dt = DateTime.Now
// Now you can pass the pointer to 'dt'
f &dt
If you are only writing the pointer instead of reading it, consider using outref<'T>
instead of byref<'T>
.
Consider the following code:
let f (x: inref<SomeStruct>) = x.SomeField
Semantically, this means the following:
- The holder of the
x
pointer may only use it to read the value. - Any pointer acquired to
struct
fields nested withinSomeStruct
are given typeinref<_>
.
The following is also true:
- There is no implication that other threads or aliases do not have write access to
x
. - There is no implication that
SomeStruct
is immutable by virtue ofx
being aninref
.
However, for F# value types that are immutable, the this
pointer is inferred to be an inref
.
All of these rules together mean that the holder of an inref
pointer may not modify the immediate contents of the memory being pointed to.
The purpose of outref<'T>
is to indicate that the pointer should only be written to. Unexpectedly, outref<'T>
permits reading the underlying value despite its name. This is for compatibility purposes.
Semantically, outref<'T>
is no different than byref<'T>
, except for one difference: methods with outref<'T>
parameters are implicitly constructed into a tuple return type, just like when calling a method with an [<Out>]
parameter.
type C =
static member M1(x, y: _ outref) =
y <- x
true
match C.M1 1 with
| true, 1 -> printfn "Expected" // Fine with outref, error with byref
| _ -> printfn "Never matched"
C# supports the in ref
and out ref
keywords, in addition to ref
returns. The following table shows how F# interprets what C# emits:
C# construct | F# infers |
---|---|
ref return value |
outref<'T> |
ref readonly return value |
inref<'T> |
in ref parameter |
inref<'T> |
out ref parameter |
outref<'T> |
The following table shows what F# emits:
F# construct | Emitted construct |
---|---|
inref<'T> argument |
[In] attribute on argument |
inref<'T> return |
modreq attribute on value |
inref<'T> in abstract slot or implementation |
modreq on argument or return |
outref<'T> argument |
[Out] attribute on argument |
An inref<'T>
type is inferred by the F# compiler in the following cases:
- A .NET parameter or return type that has an
IsReadOnly
attribute. - The
this
pointer on a struct type that has no mutable fields. - The address of a memory location derived from another
inref<_>
pointer.
When an implicit address of an inref
is being taken, an overload with an argument of type SomeType
is preferred to an overload with an argument of type inref<SomeType>
. For example:
type C() =
static member M(x: System.DateTime) = x.AddDays(1.0)
static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
static member M2(x: System.DateTime, y: int) = x.AddDays(1.0)
static member M2(x: inref<System.DateTime>, y: int) = x.AddDays(2.0)
let res = System.DateTime.Now
let v = C.M(res)
let v2 = C.M2(res, 4)
In both cases, the overloads taking System.DateTime
are resolved rather than the overloads taking inref<System.DateTime>
.
In addition to the byref
/inref
/outref
trio, you can define your own structs that can adhere to byref
-like semantics. This is done with the IsByRefLikeAttribute attribute:
open System
open System.Runtime.CompilerServices
[<IsByRefLike; Struct>]
type S(count1: Span<int>, count2: Span<int>) =
member x.Count1 = count1
member x.Count2 = count2
IsByRefLike
does not imply Struct
. Both must be present on the type.
A "byref
-like" struct in F# is a stack-bound value type. It is never allocated on the managed heap. A byref
-like struct is useful for high-performance programming, as it is enforced with set of strong checks about lifetime and non-capture. The rules are:
- They can be used as function parameters, method parameters, local variables, method returns.
- They cannot be static or instance members of a class or normal struct.
- They cannot be captured by any closure construct (
async
methods or lambda expressions). - They cannot be used as a generic parameter.
This last point is crucial for F# pipeline-style programming, as |>
is a generic function that parameterizes its input types. This restriction may be relaxed for |>
in the future, as it is inline and does not make any calls to non-inlined generic functions in its body.
Although these rules strongly restrict usage, they do so to fulfill the promise of high-performance computing in a safe manner.
Byref returns from F# functions or members can be produced and consumed. When consuming a byref
-returning method, the value is implicitly dereferenced. For example:
let squareAndPrint (data : byref<int>) =
let squared = data*data // data is implicitly dereferenced
printfn $"%d{squared}"
To return a value byref, the variable that contains the value must live longer than the current scope.
Also, to return byref, use &value
(where value is a variable that lives longer than the current scope).
let mutable sum = 0
let safeSum (bytes: Span<byte>) =
for i in 0 .. bytes.Length - 1 do
sum <- sum + int bytes[i]
&sum // sum lives longer than the scope of this function.
To avoid the implicit dereference, such as passing a reference through multiple chained calls, use &x
(where x
is the value).
You can also directly assign to a return byref
. Consider the following (highly imperative) program:
type C() =
let mutable nums = [| 1; 3; 7; 15; 31; 63; 127; 255; 511; 1023 |]
override _.ToString() = String.Join(' ', nums)
member _.FindLargestSmallerThan(target: int) =
let mutable ctr = nums.Length - 1
while ctr > 0 && nums[ctr] >= target do ctr <- ctr - 1
if ctr > 0 then &nums[ctr] else &nums[0]
[<EntryPoint>]
let main argv =
let c = C()
printfn $"Original sequence: %O{c}"
let v = &c.FindLargestSmallerThan 16
v <- v*2 // Directly assign to the byref return
printfn $"New sequence: %O{c}"
0 // return an integer exit code
This is the output:
Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence: 1 3 7 30 31 63 127 255 511 1023
A let
-bound value cannot have its reference exceed the scope in which it was defined. For example, the following is disallowed:
let test2 () =
let x = 12
&x // Error: 'x' exceeds its defined scope!
let test () =
let x =
let y = 1
&y // Error: `y` exceeds its defined scope!
()
This prevents you from getting different results depending on if you compile with optimizations or not.
.NET feedback
.NET is an open source project. Select a link to provide feedback: