Replies: 6 comments
-
Estimated reading time: 70 minutes, 51 seconds. Contains 14173 words |
Beta Was this translation helpful? Give feedback.
-
Browsed through this proposal, but isn't this similar to #242 ? |
Beta Was this translation helpful? Give feedback.
-
#242 is relatively new. No doubt they are similar. |
Beta Was this translation helpful? Give feedback.
-
Is it valid to move a variable into a closure, then execute the lambda multiple times? move object x = new object();
var lambda = () => return move x;
// Both y and z will have ownership of x
move object y = lambda();
move object z = lambda(); |
Beta Was this translation helpful? Give feedback.
-
Moving a variable into a closure just by capturing the variable seems to be too tricky. Let’s say this is not allowed. But the following is allowed: move object x = new object();
var lambda1 = () => return move x; //error: cannot move-return x because x is not owned by lambda1
var lambda2 = (move object arg) => return move arg;//ok
move object y = lambda2(move x);//y owns x
move object z = lambda2(move x);//error |
Beta Was this translation helpful? Give feedback.
-
@dmitrykm Why not just use |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Static analysis to simplify resource management
Download this proposal as html (styled with css, text wrapping works):
Download this proposal as a set of markdown files:
Introduction
GC solves the problem of managing RAM occupied by purely managed objects, but is useless for managing other types of resources.
Most resources used by applications require deterministic disposal.
C#
solves this problem by introducing theIDisposable
interface, but using this interface correctly is hard due to limited support from the language.This proposal focuses on making manual deterministic resource management easier.
Destructible Types #161 in Roslyn repo and Destructible Types #121 in csharplang repo address the problem of deterministic resource management by introducing Destructible Types and prohibiting to copy their values, while reserving the
move
keyword for ownership transfer. Thus Destructible Types are introduced directly to represent linear types.This proposal instead models linear transfer of ownership by putting all the necessary constraints into separate keywords. The keywords act in conjunction with ordinary variables and types, while the types themselves do not carry any ownership constraints.
This proposal is built around the
IDisposable
interface becauseC#
currently does not support destructors. All reasoning and examples here rely onIDisposable
. That is done so becauseIDisposable
is already a part of.net
andC#
, and it is easier to understand code samples that use familiarC#
features.Types with destructors bound to stack and called automatically by the compiler allow very concise, clear, exception-safe code as in
C++
and without constructs liketry-finally
orusing
.On the other hand, keywords introduced by the current proposal are compatible with existing types and annotate ownership semantics in every place and context.
If any of the proposals, Destructible Types #161 in Roslyn repo, Destructible Types #121 in csharplang repo , or the current one is implemented, that will dramatically improve developers' experience.
This proposal focuses on compiler enforced resource tracking that is statically provable to be correct.
C++
has destructors, yet it needed to introduce smart pointers. This proposal starts from reasoning about smart pointers and then shows that in most cases it is possible to abandon smart pointers in favor of built-in compiler support, and manage resources easier and with less errors than smart pointers allow.This proposal extends Proposal: move #160 with one more keyword, more syntactic contexts and use cases for the keywords, more constraints and static analysis.
Problems:
Dispose(...)
on objects of disposable types and thus does not enforce resource disposalDispose(...)
IDisposable
are not assumed to be disposed of by client code. Instead, those objects are either managed by a library/framework/other sides or implementIDisposable
because some framework requires doing so, not because they manage resources and really need disposal. But from the language's standpoint, it is impossible to distinguish such objects from objects that really require disposing.using
-statement. Instead, such resources need to be held in class fields or properties.The
using
-statement cannot be applied to such use cases, and so the language implicitly discourages them, despite their necessity.resource, who owns it and who must or must not dispose of it.
Such arguments are also taken out of the context where the resources they carry
are allocated and deallocated. Tracking the "world lines" and lifetimes of resources whose use is scattered across multiple methods and classes quickly
becomes hard. Needless to say, users of a resource can save their own references to the resource,
resulting in multiple references to the same resource.
through interfaces between separate components/subsystems developed by different teams at different times. Different teams adopt different resource management conventions. Currently conventions and documentation is the only aid in resource management, and it is highly fragile and unreliable without being enforced by the compiler.
This proposal addresses all of the above.
Exclusive ownership
Exclusive ownership is a well-known and time-proven resource management model. Each resource has exactly one owner. The owner is responsible for disposing of the owned resource.
unique_ptr
inC++
is a popular implementation of the model.Here is simple interface to represent the model:
Users of the resource see it through "resource views". Each user has its own resource view. The resource can have many users and many resource views, but only one of the users (and resource views) is the resource owner (
IXOwnershipResourceView.IsOwner == true
), while others are merely allowed to use the resource as non-owners.When non-owners call
IXOwnershipResourceView.Dispose()
, their resource views do nothing, while callingIXOwnershipResourceView.Dispose()
on the owning resource view disposes of the underlying resource.The owner may decide to transfer the ownership to someone else. Calling
IXOwnershipResourceView.Move()
transfers the ownership from the original resource view to a newly instantiated one and returns the latter.The owner may decide to allow other parties to use the resource without ownership transfer. Calling
IXOwnershipResourceView.Lease()
creates and returns a new non-owning resource view for such cases.Owning resource views can move and lease underlying resources to other resource views, while non-owning resource views can only lease underlying resources to other resource views.
The first instance of
IXOwnershipResourceView
created for a resource instance ("root" resource view) automatically becomes the owner. The root resource view can either be created using a dedicated constructor in a class implementingIXOwnershipResourceView
or using a dedicated extension method:All other instances of
IXOwnershipResourceView
for the same instance of the resource shouldbe derived via
IXOwnershipResourceView.Move()
andIXOwnershipResourceView.Lease()
, to simplify the implementation.Resource view derivation paths are flexible and in most cases form trees, e.g:

Example of resource view derivation
However, the absence of loops is not a requirement and paths like token ring are allowed.
Optimizing IXOwnershipResourceView
IXOwnershipResourceView
as defined above does not differentiate owning and non-owning resource views at compile time.Such flexibility is sometimes needed. If there are two parties,
producer
andconsumer
,producer
can sometimes send products along with their ownership toconsumer
, while just lending products toconsumer
in other cases:However, we often know at compile time who is the resource owner in a particular method or class, because the program was designed accordingly. For such cases it would make sense to split
IXOwnershipResourceView
into two interfaces specific to owners and non-owners:The next optimization step is notice that
IXOwningResourceView
andIXNonOwningResourceView
are not needed at all. They are merely resource holders and serve as markers that a particular resource is owned or not owned by some method argument, local variable, class field or property.IXOwningResourceView
andIXNonOwningResourceView
statically declare possibletopologies of resource view derivation. We remove
IXOwningResourceView
andIXNonOwningResourceView
as it is sufficient to use themove
andlease
annotations to describe thesame topology to the compiler, without loosing any useful information.
Moving assignments
A moving assignment transfers a value (resource) and its ownership from the assignment source to the
assignment destination:
As soon as the ownership is transferred to
dst
,dst
can do anything with the resource, including instantly disposing of it. This means that right after the moving assignment,src
can no longer use or reference the resource. Moving assignment acts as an unidirectionally opaque wall wheresrc
is not allowed to observe what happens to the resource and how it is used after the moving assignment.In some cases move-assignment sources can be referenced in code after being moved. The compiler resets the source's previous value by assigning
default(TSrc)
to the source in such cases:Move-assignment sources whose moved values can be observed are ("observable moved sources"):
managed by
using
-blocksmove-ref
arguments of methods.In other cases the compiler statically enforces that no-longer-valid values of moved assignment sources cannot be observed anywhere in code. In such cases every moved source is marked as uninitialized right after being moved. Trying to read such a source generates a compilation error. However, it is not prohibited to reinitialize the source with another value in order to reuse it.
Move-assignment sources whose moved values cannot be observed are ("unobservable moved sources"):
using
-blocksmove
andIDisposable
When
move
is used with types implementingIDisposable
, the compiler performs additional analysis to prevent resource leaks.Any owner of a disposable resource marked with
move
should either dispose of the resource directly or transfer this responsibility to another owner with the help ofmove
.Proposal core
The proposal consists of introducing:
move
andlease
IDisposable
move
The
move
andlease
keywords can appear in different contexts.move
andlease
as variable declaration specifiersmove
in declarations of local variables, method parameters, class fields, class properties designates that the declared variable, method parameter, class field or property owns its value, whilelease
designates that the declared variable, method parameter, class field or property uses its value but does not own it:Declarations with neither
move
norlease
do not convey meaning related to ownership.move
andlease
as modifiers of the assigment operatormove
andlease
appearing right after the assignment operator modify the assignment semantics accordingly.Lease-assignment means that the source variable leases its value to the destination variable. The destination is then allowed to use the value while not owning it:
Lease-assignment does not involve any run time changes and acts at run time exactly as
the ordinary assignment specific to the types of the source and destination. All the changes are used only for additional static analysis.
Move-assignment means that the source variable moves its value to the destination variable.
The destination becomes the new owner of the value, while the source no longer owns the value:
At run time moving assignment looks like the ordinary assignment specific to the types of the source and destination, followed by assigning
default(TSourceType)
to the source when that is necessary.Other sections detail this.
Passing arguments by
move
andlease
into methods is semantically equivalent tomove
- andlease
-assignments, just differs syntactically:More details about methods:
MethodParametersAndReturns
move
andlease
are available for all typesIt is easy to reason about
move
andlease
in terms of resources, especially in the context of managingIDisposable
resources. However, thanks to their highly abstract nature, it is useful to allow these keywords for all types. One example immediately follows.Assignment rules
Sources and destinations of assignments are local variables, method parameters and returns, class fields and properties.
If a source is not marked with
move
orlease
at its declaration site explicitly and does not receivemove
orlease
implicitly as a part of type inference, let us call such a source "unmarked". The two other possible states are "marked with lease" and "marked with move", no matter explicitly or implicitly.In addition, destinations can be declared using
var
.var
-declarations cannot specifymove
orlease
but can receive them from type inference.Assignment can be:
dst = src;
dst = lease src;
dst = move src;
Source: {Unmarked, Lease, Move}
Assignment: {Unmarked, Lease, Move}
Destination: {Unmarked, Lease, Move, Var}
The Cartesian product
[Source] x [Assignment] x [Destination]
consists of 36 cases.There is one exception to these 36 cases:
move
andlease
declared in method signatures are required to be exactly "mirrored" at all call sites, see "Method parameters and return values". This exception applies to all method call sites and has a higher priority than all the 36 cases. When this exception does not apply, the 36 cases work.All method call sites mirror
move
andlease
declared inmethod signatures.
dst (unmarked) = src (unmarked)
Ordinary assignment.
dst (unmarked) = src (lease)
Downgrading assignment.
Ownership metadata is lost.
Backwards compatibility.
Warning: "Downgrading assignment".
dst (unmarked) = src (move)
Downgrading assignment.
Ownership metadata is lost.
Backwards compatibility.
Warning: "Downgrading assignment".
dst (unmarked) = lease src (unmarked)
Compilation error: cannot lease-assign
unmarked source to unmarked destination.
Use ordinary assignment.
dst (unmarked) = lease src (lease)
Compilation error: cannot lease-assign to unmarked
destination. Use downgrading assignment.
dst (unmarked) = lease src (move)
Compilation error: cannot lease-assign to unmarked
destination. Use downgrading assignment.
dst (unmarked) = move src (unmarked)
Compilation error: cannot move-assign
unmarked source to unmarked destination.
Use ordinary assignment.
dst (unmarked) = move src (lease)
Compilation error: cannot move-assign
source marked with 'lease' to
unmarked destination.
Use downgrading assignment.
dst (unmarked) = move src (move)
Compilation error: cannot move-assign to unmarked
destination. Use downgrading assignment.
dst (lease) = src (unmarked)
Upgrading assignment.
Ownership metadata is introduced.
dst
is tracked further as marked withlease
.Backwards compatibility.
Warning: "Upgrading assignment"
(except when
src
is a literal or constructor call)dst (lease) = src (lease)
Compilation error: cannot assign source
marked with lease to destination marked
with lease. Use lease-assignment.
dst (lease) = src (move)
Compilation error: cannot assign source
marked with move to destination marked
with lease. Use lease-assignment.
dst (lease) = lease src (unmarked)
Upgrading lease-assignment.
Ownership metadata is introduced.
dst
is tracked further as marked withlease
.Backwards compatibility.
Warning: "Upgrading lease-assignment".
(except when src is a literal or constructor call)
/*
Compilation error: cannot lease-assign
unmarked source to marked destination.
Use upgrading assignment.
*/
dst (lease) = lease src (lease)
Lease assignment.
dst
is tracked further as marked withlease
.dst (lease) = lease src (move)
Lease assignment.
dst
is tracked further as marked withlease
.dst (lease) = move src (unmarked)
Compilation error: cannot move-assign
unmarked source to destination marked with 'lease'.
dst (lease) = move src (lease)
Compilation error: cannot move-assign
source marked with 'lease'
to destination marked with 'lease'.
Use lease-assignment.
dst (lease) = move src (move)
Compilation error: cannot move-assign
to destination marked with 'lease'.
Use lease-assignment.
dst (move) = src (unmarked)
Upgrading assignment.
Ownership metadata is introduced.
dst
is tracked further as marked withmove
.Backwards compatibility.
Warning: "Upgrading assignment".
(except when src is a literal or constructor call)
dst (move) = src (lease)
Compilation error: cannot assign source
marked with
lease
to destinationmarked with
move
.dst (move) = src (move)
Compilation error: cannot assign source marked
with
move
to destination marked withmove
.Use move assignment.
dst (move) = lease src (unmarked)
Compilation error: cannot lease-assign
unmarked source to marked destination.
Use upgrading assignment.
dst (move) = lease src (lease)
Compilation error: cannot lease-assign to
destination marked with
move
dst (move) = lease src (move)
Compilation error: cannot lease-assign to
destination marked with
move
.Use move assignment.
dst (move) = move src (unmarked)
Upgrading move-assignment.
Ownership metadata is introduced.
dst
is tracked further as marked withmove
.If
src
is a property or field, the compilerassigns
default(TResource)
tosrc
.If in addition
src
is a property, it mustexpose a setter accessible at the line
with the assignment, otherwise that is a
compilation error.
Backwards compatibility.
Warning: "Upgrading move-assignment".
(except when
src
is a literal or constructor call)/*
Compilation error: cannot move-assign
unmarked source to marked destination.
Use upgrading assignment.
*/
dst (move) = move src (lease)
Compilation error: cannot move
source marked with 'lease'
dst (move) = move src (move)
Move assignment.
dst
is tracked further as marked withmove
.If
src
is a property or field, the compilerassigns
default(TResource)
tosrc
.If
src
is a property, it mustexpose a setter accessible at the line
with the assignment, otherwise that is a
compilation error.
var dst = src (unmarked)
Ordinary assignment.
var dst = src (lease)
Compilation error: cannot assign source
marked with
lease
to destinationdeclared as
var
, use lease-assignmentvar dst = src (move)
Compilation error: cannot assign source
marked with
move
to destinationdeclared as
var
, use lease- ormove-assignment
var dst = lease src (unmarked)
Compilation error: cannot lease-assign
unmarked source to destination
declared as
var
, use upgradingassignment.
var dst = lease src (lease)
Lease assignment.
dst
is tracked further as marked withlease
.var dst = lease src (move)
Lease assignment.
dst
is tracked further as marked withlease
.var dst = move src (unmarked)
Compilation error: cannot move-assign
unmarked source to destination
declared as
var
, use upgradingassignment.
var dst = move src (lease)
Compilation error: cannot move
source marked with 'lease' (use lease-assignment)
var dst = move src (move)
Move assignment.
dst
is tracked further as marked withmove
.If
src
is a property or field, the compilerassigns
default(TResource)
tosrc
.If in addition
src
is a property, it mustexpose a setter accessible at the line
with the assignment, otherwise that is a
compilation error.
Moving assignment for local variables
Moving a local variable marks it as uninitialized:
This is OK as we initialize
silverlight
after moving:Capturing a local variable by a closure is not allowed if that variable is moved outside the closure (before or after):
The existing control flow analysis would be reused as much as possible:
If a captured variable is moved inside a closure, accessing the variable is not allowed after that:
When a local variable is managed by
using
, it becomes observable for the code generated byusing
. That forces the compiler to assigndefault(TResource)
to the variable after it is moved:In other cases moved local variables and method parameters are just marked as uninitialized and the compiler requires them to be initialized again in order to be read after the moving assignment.
Some examples:
Variables that are not marked:
Move and lease as field and property declaration attributes
In declarations of class fields and properties,
move
andlease
act as annotations describingwhether the field/property owns its value:
Non-readonly
IDisposable
fields and non-get-onlyIDisposable
auto properties declared withmove
can be assigned to many times, potentially overwriting their previous values. Special care must be taken to avoid leaks in such cases:
When a property of a disposable type is marked as
move
and declares automatically implemented getter and setter, the compiler generates the setter as in the example above.Moving values from owning properties and fields
If a property or field is marked with
move
and thus owns its value, it is allowed to move that value to another owner:After moving
h1.Res
toh2.Res
, the compiler assignsdefault(Resource)
toh1.Res
.If the source of a moving assignment is a property, the property must have a setter with enough visibility to be called at the line with the moving assignment, otherwise that is a compilation error.
Specifying
move
orlease
for property setters and getters is not supported, these keywords always apply to the whole property. When fine-grained control over access modifiers or implementations of resource accessors is needed, it is possible to implement the accessors as methods:As readonly fields and get-only auto properties marked with
move
cannot be assigned to, it is prohibited to move values from them. They are move-assigned their values in constructors and become lifelong owners:Method parameters and return values
move
andlease
at declaration vs call sitesIf a method signature contains
move
orlease
, then all call sites must exactly "mirror" the same keywords, similarly to the use ofref
andout
with method parameters. For example, methods that return values marked asmove
orlease
cannot be called without those keywords:Move-parameter
The compiler resets observable sources passed into methods as actual move-arguments:
_fileStream
in the snippet above is a class field and its value can be observed even after moving_fileStream
into the method call. For exception safety, all resetting assignments must be done after loading all actual arguments on the stack, but before transferring control to the callee.In the example below one of the arguments throws:
but
_fileStream
is not assignednull
because the exception occurs while evaluating and loading actual arguments on the stack and before any resetting assignment.If the callee's code starts executing, the callee becomes the owner of any values passed into it by moving.
The callee is then responsible for disposing of any
IDisposable
values it received, regardless of exceptions thrown while it executes:When moving unobservable sources into methods, the sources are just marked as uninitialized:
In the snippet above
using
ortry-finally
are not required. Validfs
moves intoStartLogging(...)
and is owned further by that method. IfFile.OpenWrite(..)
throws, we do not have valid file streams.Moving assignments in the
using
-statementUnobservable sources of move-assignments are marked as uninitialized after being moved.
When writing exception-safe code that needs to dispose of a source that potentially has not been moved due to exceptions, use the
using
-statement:In the snippet above,
fileStream
is managed by theusing
-statement. This makesfileStream
observableand forces the compiler to null it out after evaluating and loading on the stack all the actual arguments of
StartLogging(...)
.fileStream
is still marked as uninitialized after the moving assignment, and we cannotread
fileStream
directly without first assigning a value to it. But theusing
-statement "captured"fileStream
before the moving assignment and the statement is allowed to read and dispose offileStream
in its generated code. Thetry-catch
andtry-finally
blocks are not special cased to access possibly moved variables and theusing
-statement is the only way to definitely dispose of possibly moved sources:Move-out parameter
Formal method arguments declared as both
out
andmove
move the argument's value and ownership from the callee to the caller:How exactly the callee behaves in case of exceptions and what it assigns to its
out move
arguments is specified by users.The caller becomes the resource owner after the callee returns. If the caller neither moves nor disposes of the resource, that is a compilation error:
Move-ref parameter
When a method signature contains an argument declared as both
ref
andmove
:The actual argument's value and ownership move into the method when transferring control to the method
The formal argument's value and ownership move from the method to the actual argument when returning control from the method.
The value returned by the method in a
move-ref
argument may differ from the value passed into the method in the same argument.Besides, the method may leave the value without changes or return
null
after disposing of the original value or moving it to another owner.The compiler never resets actual
move-ref
arguments automatically at call sites, even when those arguments are observable after being moved. Instead, the callee is fully responsible for:IDisposable
values passed into the callee inmove-ref
argumentsmove-ref
arguments accordingly in all cases, including exceptions occurring while the callee executes.In the example below,
FindNext(...)
acts like an iterator when called in a loop. It fetches the nextiterated value, returns it to the caller, and takes care of disposing of the previous iterated value, so that callers do not need to worry about resource management:
The callee may move its
move-ref
arguments to other variables. As actualmove-ref
arguments can be observed after being moved by the callee, the compiler resets them. However, from the callee's perspective, such arguments are uninitialized after being moved:Move return
Methods returning a value marked as
move
return the value and its ownership to the caller.This is the most popular potential use case for this proposal, as a huge number of APIs (e.g. File.OpenWrite) assume this semantics.
If a method returns an
IDisposable
by moving, the caller should either dispose of that instance directly (by callingDispose()
) or move the instance to another owner. The caller cannot ignore the returned value.Lease-arguments
Method arguments marked as
lease
transfer values without transferring ownership.Methods are not allowed to prolong the lifetime of leased arguments
Consider that you take a lake house on lease for 30 days. You may share it with your friends, and even lease [0..all] rooms to other people for [0..30] days, but regardless of how you allocate the space-time resources, you have to return the keys to the owner in 30 days. Any new contracts between you and 3rd parties must not violate the contract between you and the owner.
A method
m
that receives a lease parameterlp
is allowed to leaselp
to other methods that it calls.m
is not allowed to prolong the lifetime oflp
by savinglp
in any class fields, properties, or capturinglp
by closures:However, class constructors may save its lease-arguments in their own fields and properties. When callers pass lease-arguments into a class constructor, the callers allow the constructed instance to use the passed lease-arguments for at least as long as the callers themselves reference the instance and use it:
While callees may not prolong the lifetime of lease-arguments, callers should guarantee that passed actual lease-arguments are alive for at least as long as the callee executes.
Lease-returns and lease-out arguments
Lease-returns and lease-out arguments mean that the callee allows the caller to use them, but not more:
Lease-ref arguments
lease-ref
arguments are not allowed:Interfaces and delegates
Whenever
move
orlease
are used in declarations of properties in interfaces, all implementations are forced to implement such properties with exactly the same specifiers:move
orlease
used in method signatures in interfaces force all implementations to do exactly the same as well:If a delegate is declared using
move
orlease
in its signature, then it can be assigned to only methods, anonymous delegates, lambda expressions that have exactly matchingmove
orlease
in their signatures:Backwards compatibility
Upgrading and downgrading assignments
If one of the sides, source or destination, is marked with
move
orlease
while the opposite side is not, the associated assignments will be upgrading or downgrading assignments.New code should avoid upgrading and downgrading assignments internally.
All upgrading and downgrading assignments should only occur at the interface between new and legacy code.
Developers are responsible for manually verifying that
move
andlease
declaration specifiers visible at the new-vs-legacy interface match the expectations and assumptions of the legacy code.Using upgrading and downgrading assignments allows to circumvent the rules of resource management and create invalid code. The compiler generates warnings when it encounters upgrading or downgrading
assignments. Each such warning should be reviewed and opted-out manually.
Compiling new source with legacy source
Methods, interfaces, delegates, class properties and fields that use
move
orlease
in their signatures or declarations require "mirroring"move
andlease
at all method call sites and all interface/delegate implementations. If legacy source needs to use the new methodMakeDryMartini(...)
, either rewrite the legacy source to usemove
at all call sites asMakeDryMartini(...)
requires or write the adapter methodMakeDryMartiniForLegacyCallers(...)
withoutmove
andlease
in its signature:Pass variables marked with
lease
ormove
into existing methodsOne more example:
Assign variables marked with
lease
ormove
to class fields and properties of existing classesAssign return values of existing methods to variables marked with
lease
ormove
Allow existing code to call methods that accept parameters marked with
lease
ormove
This is impossible directly, see "Compiling new source with legacy source"
Allow existing code to call methods that return values marked with
lease
ormove
This is impossible directly, see "Compiling new source with legacy source"
Read values of fields and properties of existing classes into variables marked with
lease
ormove
Examples
Basic synchronization using System.Threading.Monitor
One way to simplify the use of System.Monitor is to implement a simple disposable wrapper, like that is done in sqlite-net:
It is possible to evolve this pattern by adding static type safety, either using generics or creating multiple subclasses. Methods that require exclusive access to a synchronized resource then require an instance of
LockWrapper
:Everything so far plays well, especially thanks to the
using
-statement.Still there is a problem,
LockWrapper
is a reference type and it can be copied easily:Though
LockWrapper
correctly locks a resource across all threads, we failed to communicate the constraint that an instance ofLockWrapper
represents the permission to exclusively access the locked resource, and thusLockWrapper
instances themselves require exclusive ownership:With the second implementation, we get a compile-time error.
The lifetimes of
LockWrapper
instances in the first implementation can be managed only using theusing
-statement. Everything else is too hard and error-prone.The second implementation encourages exploring other possibilities too, like holding a
LockWrapper
in a class field:Protecting confidential information with
move
Consider an application that processes confidential data. The data is always stored encrypted on disk and only exists as plaintext in RAM. As soon as a data processing task is complete, the data in RAM is encrypted and saved. As an additional precaution, the application erases plaintext in RAM in order to prevent accidental leaks (accidental crash dumps, heartbleed, other vulnerabilities).
The application uses a custom class called
SecureStr
.SecureStr
keeps plaintext in RAM.SecureStr
implementsIDisposable
and wipes its data from RAM on disposing.SecureStr
pins its underlying buffer in memory in order to prevent the GC from creating deep copies while compacting object heaps.Assume that the application keeps its data as
json
. A file is first decrypted into aSecureStr
instance containingjson
. Thenjson
is parsed into tokens. Some tokens contain actual content:Once
json
has been parsed into tokens, the buffer withjson
is no longer used and the application disposes of it, erasing its data from memory.The token sequence is then transformed into an AST. Leaf nodes of the AST contain actual content (the same content as respective content tokens contain):
Once the AST has been built, the tokens are no longer used and the application disposes of them. There is no need to copy the content from content tokens to AST leaves. Instead,
Token.Content
is just moved to its new owner,Leaf.Content
. Disposing of content tokens does not erase anything, as the content was moved to the new owner andToken.Content == null
.AST is then transformed into a hierarchy of business entities specific to the application domain. AST leaves carry the values of properties of business-entities:
Once business entities are built, the AST is no longer used and the application disposes of it. There is no need to copy the content from AST leaves to fields of business entities. Instead,
Leaf.Content
is just moved to one of the fields of a business entity and that entity becomes the content owner.When business entities are no longer needed, the application disposes of them. As they own all confidential content, they dispose of all
SecureStr
instances and thus actually erase the content in RAM. There are no other deep copies of content in RAM, as:move
keyword supports only exclusive ownership, and business entities were the last owners.SecureStr
is a reference type and assigning it only copies references.move
is neither disposed of nor moved, the compiler generates an error or warning. This prevents accidental errors like forgetting to dispose of a property.Server architectures with data processing pipelines
A server application that needs to process requests is structured as a set of handlers, in the spirit of the chain of responsibilities pattern.
Each handler plays a specific role in processing. An incoming request is sometimes handled by several handlers to accomplish its goal.
However, it is not safe to process the same instance of request by multiple concurrently running handlers. Instead, a handler exclusively owns a request while processing it. When a handler is done with a request, it moves the request and its ownership to another handler or centralized dispatcher. Thread synchronization could be implemented similarly to one of the examples above.
Compare this to the Go's
A similar Go server would organize handlers as goroutines and exchange requests between handlers through channels. Synchronization, when necessary, would be achieved using synchronous channel operations.
Simulating discrete material flows
The semantics of exclusive ownership backed by the
move
keyword is applicable to problems involvingphysical movement of discrete material bodies between discrete places:
Conclusions
Open questions
This proposal is far from being complete or exhaustive.
Some open questions are:
move
/lease
and overloading resolutioncollections of disposable resources and
move
/lease
move
/lease
withTask<T>
andawait/async
move
/lease
andSystem.ValueTuple
C++
andunique_ptr
Returning
unique_ptr
from a function inC++
clearly communicates that the function transfers a value and its ownership to the caller.However,
unique_ptr
starts failing as soon as we want to allow someone else to use the contents ofunique_ptr
without transferring the ownership from the current owner. The sample copied from here shows the options we have in this case:All the options are obscure. Option 1 is the worst as it can mean absolutely anything in terms of ownership.
But the biggest problem is that
unique_ptr
andC++
do not offer even basic protection against incorrect use:C++
is helpless in preventing at compile-time even most trivial and obvious bugs, despite having suchpowerful and relevant features as destructors, copy constructors, copy assignment operators, move constructors,
static_assert
, preprocessor macros.These examples are provided to show that resource management cannot be expressed in terms of other language features and needs dedicated, built-in support from the compiler.
Value propositions
Thanks to the highly abstract nature of the proposed features, they can be applied to problems as low-level as managing system resources and as high-level as modeling some aspects of business logic and relations between business entities.
Abstract concepts that can be modeled in terms of this proposal include:
The
move
andlease
keywords communicate developers' intentions to other humans, thus contributing to code clarity and readability.At the same time, the keywords provide to the compiler so much missing metadata about resource "world lines" from instantiation to disposal and empower the compiler to perform rich static analysis of resource management.
Most resource management errors that currently can only be revealed at run-time using unit tests and load tests will instead be revealed at compile-time.
The ultimate goal of the proposed features is to allow authoring resource managing source code as
correct-by-design as possible.
Beta Was this translation helpful? Give feedback.
All reactions