[cfe-dev] [analyzer] Temporaries.
Artem Dergachev via cfe-dev
cfe-dev at lists.llvm.org
Wed Mar 21 20:45:56 PDT 2018
== Copy elision ==
= Why? =
I've underestimated the importance of implementing copy elision at first.
Eliding copies changes observable behavior of the program if the
copy/move constructor has side effects. Whether to elide creation of a
temporary object in certain operations is up to implementation, but most
implementations currently support and prefer copy elision, so if we
don't do the same in the analyzer we'd be analyzing a different program,
and users who don't care about portability may become grumpy because
they are sure that copy elision is happening.
Additionally, copy elision makes paths longer - including the number of
diagnostic pieces - which makes reports harder to understand and
exhausts the analyzer's budget faster.
= How? =
Consider an example:
1 class C {
2 public:
3 C(int, int);
4 C(const C &);
5 ~C();
6 };
7
8 void foo() {
9 C c = C(123, 456);
10 }
The respective AST:
DeclStmt 0x7fe1d3001ed8 <line:9:3, col:20>
`-VarDecl 0x7fe1d3001d08 <col:3, col:19> col:5 c 'C' cinit
`-ExprWithCleanups 0x7fe1d3001ec0 <col:9, col:19> 'C'
`-CXXConstructExpr 0x7fe1d3001e88 <col:9, col:19> 'C' 'void
(const C &)' elidable
`-MaterializeTemporaryExpr 0x7fe1d3001e70 <col:9, col:19>
'const C' lvalue
`-ImplicitCastExpr 0x7fe1d3001e58 <col:9, col:19> 'const
C' <NoOp>
`-CXXBindTemporaryExpr 0x7fe1d3001e38 <col:9, col:19>
'C' (CXXTemporary 0x7fe1d3001e30)
`-CXXTemporaryObjectExpr 0x7fe1d3001db8 <col:9,
col:19> 'C' 'void (int, int)'
|-IntegerLiteral 0x7fe1d3001d78 <col:11> 'int' 123
`-IntegerLiteral 0x7fe1d3001d98 <col:16> 'int' 456
And the CFG:
[B1]
1: 123
2: 456
3: C([B1.1], [B1.2]) (CXXConstructExpr, [B1.4], [B1.6], class C)
4: [B1.3] (BindTemporary)
5: [B1.4] (ImplicitCastExpr, NoOp, const class C)
6: [B1.5] (MaterializeTemporaryExpr)
7: [B1.6] (CXXConstructExpr, [B1.8], class C)
8: C c = C(123, 456);
9: ~C() (Temporary object destructor)
10: [B1.8].~C() (Implicit destructor)
This explains pretty well what we should do if we don't do copy elision:
we construct a temporary on B1.3 that's destroyed on B1.9 after being
materialized into an lvalue that's referenced by the copy-constructor
parameter; the copy-constructor will copy the object into the actual
variable.
Now, if we are to elide the copy constructor, the original constructor
C(123, 456) should construct the variable directly, and all statements
in between (B1.4, B1.5, B1.6, B1.7) and the destructor (B1.9) turn into
no-op.
I can imagine two ways of implementing such behavior:
(i). Completely remove elided elements B1.4, B1.5, B1.6, B1.7, B1.9 from
the CFG.
(ii). Keep CFG roughly the same but model elided elements as no-op in
the analyzer.
I believe that approach (ii) is ultimately better, even though (i)
sounds very tempting.
Approach (i) assumes that the modified CFG would kinda look like this:
[B1]
1: 123
2: 456
3: C([B1.1], [B1.2]) (CXXConstructExpr, [B1.4], class C)
4: C c = C(123, 456);
5: [B1.4].~C() (Implicit destructor)
Our CFGConstructor element [B1.3] will essentially be frankensteined
from *inner* CXXTemporaryObjectExpr 0x7fe1d3001db8 (it's printed as
CXXConstructExpr in those CFG dumps) and the construction context of the
*outer* copy-constructor CXXConstructExpr 0x7fe1d3001e88.
This CFG looks pretty good on its own. It's almost indistinguishable
from the CFG for 'C c(123, 456)' and contains all that's needed to
properly initialize 'c'.
However the more i think about it, the more it seems to violate the
principle of least astonishment. For instance,
getSVal(DeclStmt->getSingleDecl()->getInit(), LC) would yield UnknownVal
because the constructor that has initialized the variable is different
from the one that's present in the initializer of the variable. The code
to obtain the "actual" initializer expression may be very complicated
because initializers can be quite complex (imagine a ?: operator; the
code for extracting the actual initializer will essentially duplicate
the code for finding construction contexts).
Essentially, all of the expressions between the DeclStmt and the
CXXTemporaryObjectExpr will never be set in the Environment and would be
unknown. Any user (imagine a checker) who will try to understand what is
going on in the current program point by exploring values of expressions
will fail here.
We are going to evaluate our DeclStmt without evaluating its immediate
sub-expressions. Why would the sub-sub-sub-sub-sub-expression that we
need to evaluate the DeclStmt be still kept alive in the Environment?
If we try to make checkers' life easier by setting values in the
Environment anyway for expressions that aren't present in the CFG, how
would liveness analysis know when to garbage-collect them given this
analysis is also CFG-based?
In fact I'm quite scared to imagine how our liveness analysis would
react when he realizes that there are a lot of expressions right in the
middle of a feasible path that are completely omitted from the CFG. I
guess we are omitting some dead paths from the CFG, but omitting
expressions in the middle of a statement will almost certainly make
liveness analysis more complicated.
Long story short, approach (i) violates a large amount of very basic and
intuitive invariants that we've so far maintained. Which is scary and
will make further development harder, even if we manage to make it work.
Approach (ii) is much more in the spirit of C++ standard
[class.copy.elision]:
> "the implementation treats the source and target of the omitted
copy/move operation as simply two different ways of referring to the
same object".
Essentially, copy elision is an aliasing relationship between the stack
variable 'c' and the temporary object within its initializer. This makes
perfect sense in the CodeGen where both of these are just some stack
addresses. In analyzer terms, we declare that the CXXTempObjectRegion
for our CXXTemporaryObjectExpr and the VarRegion for our variable 'c'
are the same region. Because we don't have aliasing / renaming support
in the analyzer, we take the route of picking our regions correctly from
the start (that's, in some sense, the whole point of this construction
context hassle! - avoiding the need to rename regions later - which
would have probably been the right thing to do even if we could rename
regions). It leaves us with the following values for the expressions in
the Environment:
CXXTemporaryObjectExpr -> lazyCompoundVal{VarRegion 'c', Store after
construction}
CXXBindTemporaryExpr -> lazyCompoundVal{VarRegion 'c', Store after
construction}
MaterializeTemporaryExpr -> c
CXXConstructExpr -> lazyCompoundVal{VarRegion 'c', Store after construction}
Only CXXTemporaryObjectExpr will be actually modeled (hopefully by
inlining the constructor), with VarRegion 'c' acting as this-argument.
This leaves us with CXXConstructExpr that constructs into its own source
argument, which is exactly what the standard expects from us, even if
it's a bit amusing. It would not construct anything, of course, just
bind the value. It is questionable whether we need a checker callback
for the elided constructor (and if we do, which ones specifically).
CFG needs to be extended to add more context to the
CXXTemporaryObjectExpr in order to execute approach (ii) correctly. We
still need access to the CXXConstructExpr's construction context in
order to perform the very first construction, so i guess that the
construction context for CXXTemporaryObjectExpr would be a combination
of both. Note that there may be more than one elided copy constructor
(eg. there are two if the initializer is a ?: operator - and, well,
there may be multiple nested ?: operators).
Additionally, when we model elided expressions, we need to know that
they are elided by looking at them. We may add this information to their
CFG elements (this, again, looks tempting, but requires more CFG element
kinds) or mark expressions as elided in the program state (which we can
easily do because they are already mentioned in one of the construction
contexts).
More information about the cfe-dev
mailing list