TL;DR
We propose a new Clang annotation, [[clang::lifetime_capture_by(X)]]
, to handle cases where a function parameter’s reference is captured by entities like this
or other function parameters. This annotation helps diagnose common issues like use-after-free and dangling references that are not detected by [[clang::lifetimebound]]
.
The latest version of this proposal is being maintained at:
Introduce [[clang::lifetime_capture_by(X)]] - Google Docs.
Background
The [[clang::lifetimebound]]
attribute allows developers to indicate that certain function parameters or implicit object parameters (such as this) may have their reference captured by the return value. By specifying such a contract, Clang can detect several use-after-free (UaF) and use-after-return (UaR) errors.
However, [[clang::lifetimebound]]
has its limitations. It only accounts for lifetimes tied to return values, leaving a critical gap: it does not cover scenarios where references or pointers are captured by entities like this object, class members, or through propagation in function calls.
Lifetime contracts
Below are few examples of lifetime contracts and violations:
(Today, Clang can diagnose many of them using lifetime annotations and some limited statement-local lifetime analysis.
Violations which cannot be detected using statement-local analysis, needing complex control-flow and dataflow analysis, are explicitly out of scope of this document.)
1. Function’s return value references a parameter
When the return value captures a reference to a parameter, that parameter must outlive the return value. Annotating with [[clang::lifetimebound]]
enables Clang to diagnose certain stmt-local cases when the return value outlives the captured argument.
const std::string& getElement(const std::vector<std::string>& v [[clang::lifetimebound]],
std::size_t index) {
return v[index];
}
const std::string& use() {
const std::string& i1 = getElement({"ab", "cd"}, 0);
// ^ captures dangling reference to a temporary. Clang detects it. Good.
std::vector<std::string> v = {"ab", "cd"};
if (foo()) return getElement(v, 0);
// ^ returns dangling reference to a stack-variable. Clang detects it. Good.
const std::string& i2 = getElement(v, 0); // OK.
return i2;
// ^ returns reference to a stack-variable. Clang cannot detect this. Bad.
// Needs dataflow analysis. (Out-of-scope of this proposal).
}
2. Member function’s return value references this object
In this case, the return value of a member function captures a reference to this object. Clang supports diagnosing such cases by annotating the member function with [[clang::lifetimebound]].
struct S {
int& get() [[clang::lifetimebound]] { return x; };
int x;
};
int& use() {
const int& x = S{1}.get();
// ^ captures reference to a temporary. Clang detects it. Good.
S s{1};
return s.get();
// ^ returns reference to a stack variable. Clang detects it. Good.
}
3. this object references a parameter
In this case, a member function captures a reference to an argument in this object. To be valid, the argument should outlive the object.
Clang does not support this today. (Supporting this is the primary purpose of this document.)
struct S {
void set(const std::string& x) { this->x = x; };
std::string_view x;
};
std::string create() { return "42"; }
S use() {
S s;
s.set(create());
// ^ 's' captures a reference to a temporary. Clang cannot detect this. BAD.
std::string local = create();
s.set(local); // OK.
return s;
// ^ returns a reference to 'local'. Clang cannot detect this. Bad.
// Needs dataflow analysis. (Out-of-scope of this proposal).
}
4. A parameter references another parameter
// 'set' captures a reference to 's'
void addToSet(std::string_view s, std::set<std::string_view>& set) {
set.insert(s);
}
void use() {
std::set<std::string_view> set;
addToSet(create(), set); // Dangling. No support in clang.
}
5. Global entity references another parameter
std::set<std::string_view> set;
// 'set' captures a reference to 's'
void addToSet(std::string_view s) {
set.insert(s);
}
void use() {
addToSet(create(), set); // Dangling. No support in clang.
}
Proposal
The primary goal of this proposal is to extend Clang’s capabilities to cover cases like those in examples #3, #4, and #5. These cases involve capturing references to parameters by member functions, other parameters, or global entities, areas that are not currently handled by Clang’s [[clang::lifetimebound]]
annotation.
We propose introducing a new Clang annotation, [[clang::lifetime_capture_by(X)]]
, to formally establish and enforce these lifetime contracts.
Annotation
A function parameter Y
can be annotated with [[clang::lifetime_capture_by(X)]]
to indicate that a reference to the argument to Y
is captured by the entity X
. This establishes the contract that Y
should outlive X
.
Here X can be:
this
(for member functions).- Another named parameter of the same function.
- Empty / “Unknown”. This would be considered in global scope by the analysis.
// 3. 'this'.
struct S {
void set(const std::string& x [[clang::lifetime_capture_by(this)]]) {
this->x = x;
};
std::string_view x;
};
// 4. Another parameter.
void addToSet(std::string_view s [[clang::lifetime_capture_by(set)]],
std::set<std::string_view>& set) {
set.insert(s);
}
// 5. Global scope.
std::set<std::string_view> set;
void addToSet(std::string_view s [[clang::lifetime_capture_by()]]) {
set.insert(s);
}
Annotation in definition vs. declaration
- If the function definition does not have a parameter annotated then the canonical declaration would be used for reading the annotation for that parameter.
- If the definition has the annotated parameter, it should be consistent with the declaration and should establish the same lifetime contracts.
- The capturing parameter name
X
used in the annotation can differ in definition and declaration if the paramX
is named differently in the definition and declaration.
void addToSet(std::string_view s [[clang::lifetime_capture_by(set1)]],
std::set<std::string_view>& set1);
void addToSet(std::string_view s [[clang::lifetime_capture_by(set2)]],
std::set<std::string_view>& set2) {
set.insert(s);
}
Support in Clang
When a parameter Y
is annotated with [[clang::lifetime_capture_by(X)]]
, clang would detect instances when the argument to Y
does not outlive X
.
The implementation for this analysis would reuse the existing clang’s statement-local lifetime analysis. Since the analysis is restricted to a statement, Clang would only detect when a temporary is used as an argument to Y
and X
lives beyond the function call (thereby capturing a dangling reference to Y
in X
).
std::string create() { return "42"; }
void addToSet(std::string_view s [[clang::lifetime_capture_by(set)]],
std::set<std::string_view>& set) {
set.insert(s);
}
void use() {
std::set<std::string_view> set;
addToSet(create(), set); // Clang would now detect this.
}
Diagnosing wrong usage
Clang would diagnose when the X
(in [[clang::lifetime_capture_by(X)]]
) does not refer to a semantically valid entity.
X
is considered valid if it is this or a named function parameter or is empty (unspecified).- A parameter
X
is considered valid if it is not marked asconst
. This is likely a wrong API definition.X
cannot be const if it captures a reference to parameterY
. - A parameter
X
should be a pointer type or a reference type or a type annotated with[[gsl::Pointer]]
.
Constructors and overlap with [[clang::lifetimebound]]
For C++ constructors, the annotations [[clang::lifetimebound]]
and new [[clang::lifetime_capture_by(this)]]
(with X=this
) would overlap and provide the same analysis and detect the same violations.
For example, below both the annotations establish the contract that a reference to param X
is captured by the object s
being constructed.
struct S {
S(const int &x [[clang::lifetimebound]]) : x(x) {}
S(const int &x [[clang::lifetime_capture_by(this)]]) : x(x) {}
int &x;
}
void use() {
S s(1); // Reference to temporary '1' captured by 's'.
}
Support for Standard Containers
This annotation would prove useful especially for cases involving Container<view types>
(like std::vector<std::string_view>
).
// Storing a dangling ref in the container.
std::vector<std::string_view> t;
t.push_back(std::string());
// ^ 't' captures a dangling reference to a temporary.
We would hardcode such STL containers and their capturing member functions to implicitly annotate their parameters as [[clang::lifetime_capture_by(this)]]
. This is how we handle standard GSLOwner types today.
Examples of such candidates would include (where T
is a pointer/view type)
std::vector<T>::push_back()
std::set<T>::insert()
Alternative [[clang::lifetime_capture]]
As discussed earlier, due to limitations of a statement-local analysis, Clang would only be able to detect temporaries being used for params marked with [[clang::lifetime_capture_by(X)]].
A potential alternative is introducing a simpler [[clang::lifetime_capture]]
annotation, which would omit specifying the entity X
that captures the reference.
Reasons for preferring lifetime_capture_by(X)
:
lifetime_capture_by(X)
offers greater clarity by explicitly defining the contract between the parameter and the entity capturing its reference. By specifyingX
, developers gain a more informative and precise tool for documenting lifetime constraints.X
would be used by the analysis to distinguish between the capturing entities. For example, we can differentiate between the argument is captured by this or any other parameter:S{}.captureToGlobal(std::string())
is invalid because reference to temporary is captured to a global scope whileS{}.captureToThis(std::string())
is valid because reference to temporary is captured by a temporary this.- Furthermore, the
[[clang::lifetime_capture_by(X)]]
annotation sets a solid foundation for future improvements in lifetime analysis, such as expanding beyond statement-local analysis to detect more intricate use-after-free and dangling reference issues. See the next section for possible future direction.
Why not Rust-like lifetimes ?
Rust-like lifetimes (proposal) are a powerful, general-purpose mechanism for managing memory safety, but there are a few reasons why this proposal does not pursue them.
- Different approach and purpose: Rust introduces lifetimes as a formal abstraction layer to track reference validity. This abstraction is fundamentally different from the current C++ static analysis in Clang, which is more focused on identifying objects and their pointers and diagnosing local issues. This proposal aims to expand this existing local reasoning by linking more objects and pointers rather than introducing a new system like Rust lifetimes.
- Incremental improvements and reusing existing analysis: Rust-style lifetimes would require a major rewrite of Clang’s static analysis system. This proposal, however, builds on Clang’s existing infrastructure. It enhances current tools to catch more memory safety issues without a complete overhaul. While Rust lifetimes would address more complex cases, they are beyond the immediate scope. The focus here is on improving the current system, not replacing it.
If and when someone implements Rust-like lifetimes, this work will become obsolete. However, we can catch at least some bugs with this approach until then.
Possible future direction
A promising direction for enhancing Clang’s lifetime analysis involves detecting more general cases where entity Y
outlives X
, even when Y
is not a temporary. Extending Clang’s current single-statement-local analysis would be key to achieving this. Consider the following example:
void use() {
std::set<std::string_view> set;
if (foo()) {
string s = create();
addToSet(s, set);
// 's' goes out of scope.
}
// 'set' now has a dangling reference to 's'.
}
void use() {
std::set<std::string_view> set;
if (foo()) {
string s = create();
addToSet(s, set);
set.clear();
// 's' goes out of scope.
}
// 'set' is valid here.
}
This would require developing an infrastructure to compare lifetimes of two entities X and Y.
We need to develop an analysis to detect control flows having “X captures a reference to Y” followed by “Y goes out of scope before X” with no “X releases the captured reference to Y” in between.
Reinitialization (like set.clear()
) is one of the ways X can release the captured reference to Y. Clang already supports [[clang::reinitializes]]
annotation to mark member functions which reinitializes an object’s lifetime.