Repository navigation
Conversation
|
@Krzmbrzl would it make sense to do copy-on-mutable-access to save on copying in trivial cases? |
I don't have experience implementing something like this but it seems like that would significantly complicate the implementation, no? Also, I would really like to not give up on the implicit conversion to EDIT: Or are you strictly speaking about the copies performed during construction of |
b542c43 to
96d4c94
Compare
09828aa to
eabb51d
Compare
|
@evaleev Here's my proposal for how to proceed with this change:
Up to this point, all changes should be backwards-compatible except for changing things like the return type of the internal Finally, changing how An alternative strategy for the final step would be to remove the In a bump to SeQuant v3, we could also perform renaming of e.g. About the nameIn the meeting If Copy vs CloneThe new Then why create a |
|
@evaleev how do you want me to proceed with this? (see writeup above) |
evaleev
left a comment
There was a problem hiding this comment.
Thanks for the write-up and the ping. I built the branch in a worktree (Debug, SEQUANT_ASSERT_BEHAVIOR=THROW) and checked each point below with a compile or runtime probe; CI is green only because the tests never exercise these paths. Verdict: needs changes before merge, and the branch has to be rebased first (540 commits behind, 7 conflicting files: constant.cpp, expr.cpp, five under export/; master now has 15 clone() const override sites that must become unique_copy, and #686 rewrites Power::flatten).
Blocking
- Container-with-container arithmetic is broken for all four operators.
a + b,a - b,a ^ bwith twoExprContainers are ambiguous between the(ExprContainer, Expr)and(Expr, ExprContainer)overloads.a * bcompiles because expr_container.hpp#L78 declares a(ExprContainer, ExprContainer)overload, but nothing defines it, so it fails at link time; line 79 re-declares the(Expr, Expr)form instead of the missing mixed one. Sum(container)andProduct(container)silently produce an empty sum/product.ExprContainerhasbegin()/end()over subexpressions, so it satisfies the range constraint of the range constructors (sum.hpp#L60, product.hpp#L53). A container holding a Variable yields a Sum of size 0 with no diagnostic; same forex<Sum>(cont).Sum::filterno longer compiles when instantiated. The transform lambda at sum.hpp#L119 returns*eby deduced value, i.e. the abstractExpr. No in-tree callers, so it is invisible until someone uses it.
Should fix
- The "move" constructor deep-copies.
ExprContainer(Expr&&)(expr_container.cpp#L31) calls the constunique_copy(); the source is left intact (probe: a Sum with 3 summands still has 3 after being "moved" into a container). Every*this = Sum(...)inside the in-place operators therefore clones the freshly built tree a second time. The explicit-copy/implicit-move split in the design rests on a move that does not exist; a real move needs a virtual rvalue hook. - Moved-from containers are undetectable. After a move or
take_expr(),expr_is null.operator*,operator->and the copy ctor have no assert, so copying a moved-from container segfaults even under THROW; onlybegin()/end()assert.ExprContainer(const ExprPtr&)with a null pointer dereferences before the assert at line 25 can fire. - Root-level
visitsemantics changed silently. Aconst ExprPtr&visitor now receivesexpr.clone()for the root (expr.hpp#L422); before it received the real object viaexprptr_from_this, so mutations through the pointer and identity checks applied. Probe: a visitor relabelling the root Variable now has no effect. The in-treeconst ExprPtr&visitors (wick.impl.hpp,utility/indices.hpp,biorthogonalization.cpp) only read, so nothing breaks today, but downstream code can. - The deprecated
hash_value(shared_ptr hasher)overload deep-clones the whole expression on every call (expr.hpp#L270) and hands the hasher a different object, so identity-keyed hashers change answers. - Explicit copy ctor makes
std::copyable<ExprContainer>false.std::vector<ExprContainer>{a}does not compile, nor will anystd::ranges/range-v3 view or algorithm requiringcopyableelements. This matters directly for the plan to store containers inSum::summands_. - Duplicated arithmetic semantics. The four in-place
ExprContaineroperators re-implement theExprPtroperator logic inexpr_ptr.cpp; the two have already diverged (container clones factors, pointer aliases them). Per AGENTS.md this should be one implementation. - No scalar arithmetic.
cont + 2does not compile whileptr + 2does, sinceexpr_operators.hppwas narrowed to exactlyExprPtr. - Test gaps: container-with-container operators,
^=,adjoint(ExprContainer), iteration,take_expr,Power::flatten(ExprContainer&),Power::unique_copywith conjugation,Sum::filter, visiting an expression not managed byshared_ptr(the removed throw).
Minor
ExprContainerList(expr_container.hpp#L67) is unused, and with an explicit copy ctor elements cannot be copied out of it.Power(lvalue_container, exp)does not compile (by-value parameter + explicit copy). Fine if intended, but should be documented.- Typos:
flatt_impl(power.cpp#L41), test sections "from conrete", "Variabkle", "from base". - Several commits fix earlier commits in the same PR ("Move begin/end impl to cpp file (and fix it)", "Fix deprecation warning in test_expr", "Include supporting header"); please squash those into the commits they fix.
- After rebase,
doc/user/guide/expressions.rstneeds the new type and the changedvisitcontract.
On the proposal
- Merge as-is: not possible; the three blocking items plus the rebase come first.
- 3.1 deprecate
ExprPtrcopy: not backwards-compatible in practice.ExprPtrListis an initializer_list, everysummands()range copies pointers, and MPQC builds with warnings as errors. It also lands before the storage change that would make it natural, so call sites get churned twice. - 3.3 remove null
ExprPtr: the accumulator idiom (ExprPtr r; for (...) r += x;) is everywhere, andExprContainerhas no empty state and no default ctor. The proposal needs an answer for that before deprecating null, beyond thecanonicalizereturn types. - 3.4 interfaces take
const Expr&: sound, but functions that replace the root (simplify,expand,Power::flatten) must takeExprContainer&, which forces callers holdingExprPtrto copy. - Final step (containers inside
Sum/Product, v3): thecopyableissue above bites here. There is also no performance evidence yet; subtree sharing and memoized hashes are lost under value semantics, so I would like abenchmarks/run before committing to it. - Name: "Container" is a poor fit because the class is itself an iterable range over subexpressions, so readers will ask "container of what".
ExprHolderavoids that; I am fine with it. - Copy vs
copy(): reasonable, but the design should state outright that the type is notstd::copyableand what that implies for ranges.
The core idea is right and the unique_copy refactor is clean. Fix the blocking items, make the rvalue path actually move, rebase, and then the migration sequence is worth settling, with 3.1 and 3.3 reconsidered as above.
aff01aa to
929166f
Compare
The new function returns a unique_ptr<Expr> instead of an ExprPtr. Reason being that this gives much more flexibility such as moving the ownership of the object out of the smart pointer or simply using as a unique_ptr. Since unique_ptr is implicitly convertible to a shared_ptr (via move ctor), conversion to ExprPtr is trivially possible. To retain compatibility with existing interface, Expr now implements a clone() function by means of the new unique_copy().
This is in the way of having expression objects that are not managed by shared_ptr
"Container" is a poor fit as the class is itself iterable over the subexpressions of the held expression, which invites the question "container of what?". ExprHolder describes its purpose (holding a single expression with value semantics) without that ambiguity. AI-assisted
With an explicit copy constructor, std::copyable<ExprHolder> is false,
meaning that e.g. std::vector<ExprHolder>{a} or range views/algorithms
requiring copyable elements can't be used with ExprHolder. This would
become a problem once Sum and Product store their subexpressions as
ExprHolder objects.
Copying an ExprHolder still performs a deep copy of the held expression,
i.e. value semantics are retained.
AI-assisted
ExprHolder(Expr &&) used to call unique_copy() as the dynamic type of the passed object is unknown and hence there was no way to move it into a new heap allocation. Therefore, the "move" constructor deep-copied the expression, which also meant that every in-place ExprHolder operator that builds a new expression and assigns it to the holder copied the freshly built tree a second time. The new pure virtual Expr::unique_move() && moves an object into a new heap allocation of its dynamic type, which allows ExprHolder(Expr &&) to perform an actual move. Every Expr subclass needs to implement it. AI-assisted
Arithmetic with two ExprHolder operands was ambiguous for +, - and ^ between the overloads taking ExprHolder for the left and for the right operand. For * a (ExprHolder, ExprHolder) overload was declared but never defined, leading to a linker error. Additionally, the (Expr, Expr) overload of operator* was declared twice whereas the mixed one was missing. Now, all four operators provide all four overloads. The left-hand side is taken by value such that rvalue holders are reused instead of copied. Additionally, arithmetic between ExprHolder and scalars (or variable labels) is now supported in the same way as it is for ExprPtr. AI-assisted
ExprHolder is iterable over the subexpressions of the held expression and therefore satisfies the range constraint of the range constructors of Sum and Product. Hence, constructing a Sum or Product from an ExprHolder silently iterated over the subexpressions of the held expression, which e.g. for a held Variable produced an empty Sum. Now, an ExprHolder is treated the same way as an ExprPtr or an Expr object, i.e. as a single summand/factor. AI-assisted
The transform lambda returned the dereferenced summand by value, i.e. an object of the abstract type Expr, which meant that Sum::filter failed to compile as soon as it was instantiated. The predicate is now applied to the dereferenced summands directly. Additionally, predicates taking a const ExprPtr & are accepted again in order to remain compatible with existing code. AI-assisted
After a move or take_expr(), an ExprHolder no longer holds an expression. Previously, only begin() and end() asserted on that, while all other ways of accessing the held expression (including copying the holder) dereferenced a null pointer. Similarly, constructing an ExprHolder from a null ExprPtr dereferenced the pointer before any check could fire. All accesses now go through accessors asserting that the holder is not in a moved-from state. AI-assisted
Covers iteration, adjoint(), Power with ExprHolder (incl. flattening and copying of conjugated powers), visiting expressions that are not managed by a shared_ptr as well as the root of those that are, and the deprecated shared_ptr-based hasher overload of Expr::hash_value. AI-assisted
A visitor's signature now determines what it may do with the visited expressions, consistently for all of them, including the expression visit() is called on, regardless of how that one is managed: - ExprPtr &: replace subexpressions (does not visit the root) - Expr &: modify in-place (non-const visit() only) - const Expr &: inspect only Since const ExprPtr & does not propagate constness to the pointee, such visitors could modify every visited expression except the root, which can't be passed as an owning pointer without requiring it to be managed by a shared_ptr. Hence, these visitors are deprecated. Until they are removed, they receive a non-owning pointer to the root. All existing visitors are migrated to the new signatures. AI-assisted
ExprHolder's in-place operators moved the held expression away before cloning the other operand, which breaks if that operand is (part of) the held expression, e.g. `h += h`. Keep the held expression alive until the result has been formed instead. Sum and Product gain append_impl/prepend_impl taking a const Expr &, which clone the operand only once and handle appending an expression to itself. Also let operator/ accept const and lvalue arithmetic operands. AI-assisted
Both report the type id of Product, hence Expr::is/as can't tell them apart from Product. Turn such uses into a compile-time error and point to is<Product>() and Product::is_commutative() instead. AI-assisted
929166f to
a0bc6ed
Compare
This is supposed to be an alternative (longer-term perhaps even replacement) for
ExprPtr. It can be used to store expressions but contrary toExprPtr, the newExprContainerhas value semantics. That is, copying the container actually copies the underlying expression. This makes things much easier to reason about and as a side-effect this fixes theconstissue ofExprPtrwhich allows you to dowhich ends up modifying the original expression
ptrwas is pointing to. Hence, withExprPtrwe don't have any way to avoid accidental modification.Note: This PR is based on top of #589