TL;DR: I want to make two changes to the noescape attribute:
- disallow freeing the memory
- allow annotating parameters of record type with
noescape
Disallowing free()
At the moment the noescape attribute explicitly allows freeing the memory. The description reads (emphasis mine):
noescapeplaced on a function parameter of a pointer type is used to inform the compiler that the pointer cannot escape: that is, no reference to the object the pointer points to that is derived from the parameter value will survive after the function returns. Users are responsible for making sure parameters annotated withnoescapedo not actually escape. Callingfree()on such a parameter does not constitute an escape.
This is in line with the LLVM attributes emitted in clang codegen, as noescape applies capture(none) , but does not apply nofree . As such the description is an accurate reflection of the current behaviour, but I would argue that this is not a very useful affordance, and that the vast majority of uses of noescape (past, present and future) would benefit more from explicitly not allowing the function to free the memory. I base this on two facts:
- Most functions don’t deallocate their parameters, and for the ones that do it’s very rare to use
noescape. I haven’t found a single example of this combination. - The combined properties of not escaping and not freeing the pointer means that the function call has no impact on the pointer’s lifetime.
1) means that although this is a source breaking change, it’s unlikely to have significant negative impact. It also means that if we decided instead to create a separate nofree attribute (this doesn’t exist in clang, only in LLVM), most uses of noescape would either require extra noise in the form of an additional attribute, or underspecify the contract of the function.
2) means that we’re providing much more useful information about the semantics than “this does not escape” does alone. It makes it easier for lifetime analyses to make firmer statements about the lifetime of the pointer: not only does it not introduce any uses or aliases after the function has returned, the pointer is also guaranteed to still be alive after the function has returned. This also means that it is okay to pass a pointer to an object on the stack, which is not the case if the callee tries to free the memory. The noescape attribute was in fact originally introduced to allow the compiler to allocate block parameters on the stack rather than the heap, and this use case constitutes almost all uses at Apple at the moment. This heap elision still works for blocks under the current semantics simply because calling free on a block pointer is never allowed, since these are automatically managed by the runtime using ref counts. Extending the semantics to forbid freeing the memory, regardless of pointer type, would be more in line with the original purpose of the attribute and could in theory unlock similar heap elisions for arbitrary allocations.
Clang’s lifetime analysis already has some basic diagnostics to detect misuses of noescape. This can be extended to also (opportunistically) diagnose freed memory for noescape parameters. As the lifetime analysis grows both in scope and in popularity, I expect noescape to gain more traction, making changes like the one outlined above harder to make later without breaking existing code. At the same time, the proposed semantics would provide strictly more information to the lifetime analysis than the existing semantics.
To be extra careful we can stagger the change such that the docs and diagnostics are updated in the next release of clang, but wait another release with updating codegen to pass nofree down to LLVM (which would make freeing the memory UB) to give users time to adjust, if someone does happen to free a noescape pointer. I have not seen anyone do that, neither internally at Apple, nor in open source code, but I think breaking changes warrant being careful anyways.
Allowing noescape on records
To enable even more uses of noescape for communicating lifetime information to the lifetime analysis I would also like to propose expanding the attribute to allow annotating record types (it currently only allows annotating pointer or reference types), to enable annotation of pointer-like types (e.g. std::span ). The semantics here would be that no pointer or reference in the object is allowed to escape. At the moment I’m not aware of any way to lower this information to LLVM for record types, but this is still relevant in and of itself purely for static analysis imho. As is the case with plain pointers, noescape does not cascade onto nested pointer levels — only the outermost pointers/references in the object. Here are a few examples to clarify:
void *g1;
size_t g2;
void foo(std::span<std::span<int>> s [[clang::noescape]]) {
g1 = s[0].data(); // Ok, nested pointer can escape
g2 = s.size(); // Ok, non-pointer value can escape
g1 = s.data(); // Not ok
}
struct MyView {
std::span<int> s;
};
std::span<int> g3;
void bar(MyView v [[clang::noescape]]) {
g1 = v.s.data(); // Not ok
g3 = v.s; // Not ok, pointer value escapes as part of span
}
This change was initially rejected because of the limited use of noescape within clang, but since it’s been picked up by the lifetime analysis I think there’s more value to users now, by preventing view-types from accidentally escaping (by other means than just through async blocks).
Bonus: memory safe interop
In addition to helping clang’s own lifetime analysis, noescape can also be used for interop with safe languages: a parameter with noescape can be passed from a safe language without violating lifetime safety, but only if that function does not free the memory. If, instead of a raw pointer, the parameter is a std::span , the function can also be made bounds safe (if built with hardened C++ and -Wunsafe-buffer-usage ). We would like to combine these two properties as part of Swift’s safe interop feature to allow spans to be passed from Swift to C++ in a memory safe manner. This way, the borrow checker can accurately track its lifetime and the user would not have to write unsafe when making the function call. I want to make it clear that I think both of these changes are valuable even when considering only C and C++ in a vacuum, but as a bonus it also provides value to the wider ecosystem interoperating with these languages.
This RFC was (partially) accepted on July 02 20026