Zero-initialize non-independent parameter adjoints and introduce clad::zero_like - #1932
Zero-initialize non-independent parameter adjoints and introduce clad::zero_like#1932fogsong233 wants to merge 6 commits into
clad::zero_like#1932Conversation
Invoke the existing zero_init customization point after copying the primal structure for an internally-created adjoint. Add an std::vector regression where the copied primal value otherwise contaminates the selected parameter's gradient.
|
clang-tidy review says "All clean, LGTM! 👍" |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
clad::zero_like
| /// size, allocator, or device information. The reverse-mode visitor prefers | ||
| /// this customization point when it must create an internal adjoint from an | ||
| /// existing primal value. | ||
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || | |
| template <class T, std::enable_if_t<std::is_arithmetic<T>::value || |
include/clad/Differentiator/Differentiator.h:218:
- int>::type = 0>
+ int> = 0>| /// size, allocator, or device information. The reverse-mode visitor prefers | ||
| /// this customization point when it must create an internal adjoint from an | ||
| /// existing primal value. | ||
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || |
There was a problem hiding this comment.
warning: use c++17 style variable templates [modernize-type-traits]
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || | |
| template <class T, typename std::enable_if<std::is_arithmetic_v<T> || |
| /// this customization point when it must create an internal adjoint from an | ||
| /// existing primal value. | ||
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || | ||
| std::is_enum<T>::value, |
There was a problem hiding this comment.
warning: use c++17 style variable templates [modernize-type-traits]
| std::is_enum<T>::value, | |
| std::is_enum_v<T>, |
| // Test overload viability before asking Sema to build the call. A failed | ||
| // optional customization must quietly fall back to the legacy adjoint | ||
| // construction path instead of producing a diagnostic. | ||
| if (UnresolvedLookup->hasPlaceholderType(BuiltinType::Overload)) { |
There was a problem hiding this comment.
warning: no header providing "clang::BuiltinType" is directly included [misc-include-cleaner]
if (UnresolvedLookup->hasPlaceholderType(BuiltinType::Overload)) {
^| // optional customization must quietly fall back to the legacy adjoint | ||
| // construction path instead of producing a diagnostic. | ||
| if (UnresolvedLookup->hasPlaceholderType(BuiltinType::Overload)) { | ||
| OverloadExpr::FindResult Find = OverloadExpr::find(UnresolvedLookup); |
There was a problem hiding this comment.
warning: no header providing "clang::OverloadExpr" is directly included [misc-include-cleaner]
OverloadExpr::FindResult Find = OverloadExpr::find(UnresolvedLookup);
^| if (UnresolvedLookup->hasPlaceholderType(BuiltinType::Overload)) { | ||
| OverloadExpr::FindResult Find = OverloadExpr::find(UnresolvedLookup); | ||
| if (!Find.HasFormOfMemberPointer && | ||
| isa<UnresolvedLookupExpr>(Find.Expression)) { |
There was a problem hiding this comment.
warning: no header providing "clang::UnresolvedLookupExpr" is directly included [misc-include-cleaner]
isa<UnresolvedLookupExpr>(Find.Expression)) {
^be04c51 to
91b0837
Compare
| /// size, allocator, or device information. The reverse-mode visitor prefers | ||
| /// this customization point when it must create an internal adjoint from an | ||
| /// existing primal value. | ||
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| template <class T, typename std::enable_if<std::is_arithmetic<T>::value || | |
| template <class T, std::enable_if_t<std::is_arithmetic<T>::value || |
include/clad/Differentiator/Differentiator.h:222:
- int>::type = 0>
+ int> = 0>|
|
||
| /// Default zero_like implementation for explicitly opted-in owning ranges. | ||
| template <class T, | ||
| typename std::enable_if< |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| typename std::enable_if< | |
| std::enable_if_t< |
include/clad/Differentiator/Differentiator.h:304:
- int>::type = 0>
+ int> = 0>| template <class T, | ||
| typename std::enable_if< | ||
| is_range<T>::value && is_copy_and_zero_range<T>::value && | ||
| std::is_copy_constructible<T>::value, |
There was a problem hiding this comment.
warning: use c++17 style variable templates [modernize-type-traits]
| std::is_copy_constructible<T>::value, | |
| std::is_copy_constructible_v<T>, |
| } | ||
|
|
||
| template <class T, class Allocator> | ||
| struct is_copy_and_zero_range<std::vector<T, Allocator>> : std::true_type {}; |
There was a problem hiding this comment.
warning: explicit specialization of undeclared template struct 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::vector<T, Allocator>> : std::true_type {};
^91b0837 to
8affac2
Compare
| #include <clad/Differentiator/BuiltinDerivatives.h> | ||
| #include <clad/Differentiator/FunctionTraits.h> | ||
| #include <deque> | ||
| #include <forward_list> |
There was a problem hiding this comment.
warning: included header deque is not used directly [misc-include-cleaner]
| #include <forward_list> | |
| #include <forward_list> |
| #include <clad/Differentiator/FunctionTraits.h> | ||
| #include <deque> | ||
| #include <forward_list> | ||
| #include <functional> |
There was a problem hiding this comment.
warning: included header forward_list is not used directly [misc-include-cleaner]
| #include <functional> | |
| #include <functional> |
| #include <initializer_list> | ||
| #include <iterator> | ||
| #include <list> | ||
| #include <map> |
There was a problem hiding this comment.
warning: included header list is not used directly [misc-include-cleaner]
| #include <map> | |
| #include <map> |
| #include <type_traits> | ||
| #include <unordered_map> | ||
| #include <valarray> | ||
| #include <vector> |
There was a problem hiding this comment.
warning: included header valarray is not used directly [misc-include-cleaner]
| #include <vector> | |
| #include <vector> |
|
|
||
| // Sequence containers whose copy constructors create independent element | ||
| // storage can use the default copy-and-zero implementation of zero_like. | ||
| template <class T, std::size_t N> |
There was a problem hiding this comment.
warning: no header providing "std::size_t" is directly included [misc-include-cleaner]
include/clad/Differentiator/STLBuiltins.h:7:
- #include <deque>
+ #include <cstddef>
+ #include <deque>| // Sequence containers whose copy constructors create independent element | ||
| // storage can use the default copy-and-zero implementation of zero_like. | ||
| template <class T, std::size_t N> | ||
| struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {}; |
There was a problem hiding this comment.
warning: explicit specialization of undeclared template struct 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {};
^| struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {}; | ||
|
|
||
| template <class T, class Allocator> | ||
| struct is_copy_and_zero_range<std::deque<T, Allocator>> : std::true_type {}; |
There was a problem hiding this comment.
warning: too few template arguments for class template 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::deque<T, Allocator>> : std::true_type {};
^Additional context
include/clad/Differentiator/STLBuiltins.h:36: template is declared here
struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {};
^| struct is_copy_and_zero_range<std::deque<T, Allocator>> : std::true_type {}; | ||
|
|
||
| template <class T, class Allocator> | ||
| struct is_copy_and_zero_range<std::forward_list<T, Allocator>> |
There was a problem hiding this comment.
warning: too few template arguments for class template 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::forward_list<T, Allocator>>
^Additional context
include/clad/Differentiator/STLBuiltins.h:36: template is declared here
struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {};
^| : std::true_type {}; | ||
|
|
||
| template <class T, class Allocator> | ||
| struct is_copy_and_zero_range<std::list<T, Allocator>> : std::true_type {}; |
There was a problem hiding this comment.
warning: too few template arguments for class template 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::list<T, Allocator>> : std::true_type {};
^Additional context
include/clad/Differentiator/STLBuiltins.h:36: template is declared here
struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {};
^| struct is_copy_and_zero_range<std::list<T, Allocator>> : std::true_type {}; | ||
|
|
||
| template <class T, class Allocator> | ||
| struct is_copy_and_zero_range<std::vector<T, Allocator>> : std::true_type {}; |
There was a problem hiding this comment.
warning: too few template arguments for class template 'is_copy_and_zero_range' [clang-diagnostic-error]
struct is_copy_and_zero_range<std::vector<T, Allocator>> : std::true_type {};
^Additional context
include/clad/Differentiator/STLBuiltins.h:36: template is declared here
struct is_copy_and_zero_range<std::array<T, N>> : std::true_type {};
^8affac2 to
9b0557b
Compare
|
|
||
| template <class T, class It> | ||
| std::integral_constant< | ||
| bool, !std::is_same<typename std::remove_cv<T>::type, |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| bool, !std::is_same<typename std::remove_cv<T>::type, | |
| bool, !std::is_same<std::remove_cv_t<T>, |
| template <class T, class It> | ||
| std::integral_constant< | ||
| bool, !std::is_same<typename std::remove_cv<T>::type, | ||
| typename iterator_traits<It>::value_type>::value> |
There was a problem hiding this comment.
warning: use c++17 style variable templates [modernize-type-traits]
| typename iterator_traits<It>::value_type>::value> | |
| bool, !std::is_same_v<typename std::remove_cv<T>::type, | |
| typename iterator_traits<It>::value_type>> |
| // Fill an array with zeros. | ||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) | ||
| unsigned char tmp[sizeof(T)] = {}; | ||
| template <class T, typename std::enable_if<!is_range<T>::value, int>::type = 0> |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| template <class T, typename std::enable_if<!is_range<T>::value, int>::type = 0> | |
| template <class T, std::enable_if_t<!is_range<T>::value, int> = 0> |
| CUDA_HOST_DEVICE void zero_impl(volatile T& t) { | ||
| // Bound once so the assertion and the guard below cannot drift apart: a | ||
| // type the assertion rejects must not go on to be memcpy'd anyway. | ||
| constexpr bool is_zeroable = std::is_trivially_destructible<T>::value; |
There was a problem hiding this comment.
warning: use c++17 style variable templates [modernize-type-traits]
| constexpr bool is_zeroable = std::is_trivially_destructible<T>::value; | |
| constexpr bool is_zeroable = std::is_trivially_destructible_v<T>; |
| if constexpr (is_zeroable) { | ||
| // Fill an array with zeros. | ||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) | ||
| unsigned char tmp[sizeof(T)] = {}; |
There was a problem hiding this comment.
warning: do not declare C-style arrays, use std::array<> instead [modernize-avoid-c-arrays]
unsigned char tmp[sizeof(T)] = {};
^| for (auto& x : t) | ||
| zero_init(x); | ||
| } | ||
| template <class T, typename std::enable_if<is_range<T>::value, int>::type = 0> |
There was a problem hiding this comment.
warning: use c++14 style type templates [modernize-type-traits]
| template <class T, typename std::enable_if<is_range<T>::value, int>::type = 0> | |
| template <class T, std::enable_if_t<is_range<T>::value, int> = 0> |
| template <class T> struct is_range; | ||
| namespace zero_like_detail { | ||
| template <class T, class> struct is_mutable_range; | ||
| } |
There was a problem hiding this comment.
warning: namespace 'zero_like_detail' not terminated with a closing comment [llvm-namespace-comment]
| } | |
| } // namespace zero_like_detail |
Additional context
include/clad/Differentiator/STLBuiltins.h:26: namespace 'zero_like_detail' starts here
namespace zero_like_detail {
^9b0557b to
f475422
Compare
| #include <iterator> | ||
| #include <list> | ||
| #include <map> | ||
| #include <memory> |
There was a problem hiding this comment.
warning: included header map is not used directly [misc-include-cleaner]
| #include <memory> | |
| #include <memory> |
| #include <tuple> | ||
| #include <type_traits> | ||
| #include <unordered_map> | ||
| #include <valarray> |
There was a problem hiding this comment.
warning: included header unordered_map is not used directly [misc-include-cleaner]
| #include <valarray> | |
| #include <valarray> |
f475422 to
5c3b836
Compare
| // NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays) | ||
| template <typename T> CUDA_HOST_DEVICE void zero_init(T* x, std::size_t N) { | ||
| for (std::size_t i = 0; i < N; ++i) | ||
| zero_init(x[i]); |
There was a problem hiding this comment.
warning: do not use pointer arithmetic [cppcoreguidelines-pro-bounds-pointer-arithmetic]
zero_init(x[i]);
^5c3b836 to
7e52112
Compare
|
clang-tidy review says "All clean, LGTM! 👍" |
vgvassilev
left a comment
There was a problem hiding this comment.
This actually looks very good!
| OverloadCandidateSet::iterator Best = nullptr; | ||
| OverloadingResult OverloadResult = | ||
| CandidateSet.BestViableFunction(m_Sema, noLoc, Best); | ||
| if (OverloadResult != 0U) |
There was a problem hiding this comment.
| if (OverloadResult != 0U) | |
| if (OverloadResult != OR_Success) |
|
|
||
| namespace clad { | ||
|
|
||
| template <class T> struct is_range; |
There was a problem hiding this comment.
Do we really need this forward declaration -- seems fragile.
| template <class T, std::enable_if_t<is_range<T>::value && | ||
| std::is_copy_constructible<T>::value, | ||
| int> = 0> | ||
| T zero_like(const T& value) { |
There was a problem hiding this comment.
This copies the whole primal only to overwrite it with zeros — for an N-element container that's N element copy-ctors + two data passes. For sized, size-constructible ranges you can build T(value.size()) and skip the copy while keeping zero_init for correctness:
From 4949edc8882e8864a0466a53390a65674907a580 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev@gmail.com>
Date: Wed, 29 Jul 2026 16:51:17 +0000
Subject: [PATCH] [Differentiator] Build sized zero_like adjoints without a
primal copy.
The default zero_like for a range copies the primal and then zeroes the
copy, so building an adjoint for an N-element container touches the data
twice and runs N element copy-constructors whose results are immediately
overwritten.
For a range that exposes size() and a size-constructor -- vector, deque,
list, valarray -- construct N value-initialized elements directly and
zero_init them; this drops the primal copy while zero_init keeps element
types that do not value-initialize to zero correct. Ranges without a
size-constructor (forward_list, std::array, the associative containers)
keep the copy-then-zero fallback.
Follows up on the zero_like customization point from #1932.
---
include/clad/Differentiator/Differentiator.h | 42 +++++++++++--
test/Misc/ZeroLikeSized.C | 63 ++++++++++++++++++++
2 files changed, 101 insertions(+), 4 deletions(-)
create mode 100644 test/Misc/ZeroLikeSized.C
diff --git a/include/clad/Differentiator/Differentiator.h b/include/clad/Differentiator/Differentiator.h
index 80f74b37..78c1584f 100644
--- a/include/clad/Differentiator/Differentiator.h
+++ b/include/clad/Differentiator/Differentiator.h
@@ -295,10 +295,44 @@ CUDA_HOST_DEVICE auto back(TapeType& of) -> decltype(of.back()) {
return T();
}
- /// Default zero_like implementation for copyable ranges.
- template <class T, std::enable_if_t<is_range<T>::value &&
- std::is_copy_constructible<T>::value,
- int> = 0>
+ // Detect a range that has size() and a T(size_type) constructor, so a zero
+ // adjoint can be built from just the size instead of copying the primal.
+ // C++14-safe overload dispatch, mirroring is_range above.
+ namespace zero_like_detail {
+ template <class T>
+ auto has_sized_ctor(int)
+ -> decltype(static_cast<void>(std::declval<const T&>().size()),
+ static_cast<void>(T(std::declval<typename T::size_type>())),
+ std::true_type{});
+ template <class T> std::false_type has_sized_ctor(...);
+ } // namespace zero_like_detail
+
+ template <class T>
+ struct has_sized_ctor : decltype(zero_like_detail::has_sized_ctor<T>(0)) {};
+
+ /// Default zero_like for a sized, size-constructible range (vector, deque,
+ /// list, valarray, ...): construct `size` value-initialized elements -- no
+ /// copy of the primal's values -- then zero_init for correctness on element
+ /// types that do not value-initialize to zero.
+ template <class T,
+ std::enable_if_t<is_range<T>::value &&
+ std::is_copy_constructible<T>::value &&
+ has_sized_ctor<T>::value,
+ int> = 0>
+ T zero_like(const T& value) {
+ T result(value.size());
+ zero_init(result);
+ return result;
+ }
+
+ /// Fallback zero_like for copyable ranges without a size-constructor
+ /// (forward_list, std::array, associative containers, ...): copy the
+ /// structure, then zero it.
+ template <class T,
+ std::enable_if_t<is_range<T>::value &&
+ std::is_copy_constructible<T>::value &&
+ !has_sized_ctor<T>::value,
+ int> = 0>
T zero_like(const T& value) {
T result(value);
zero_init(result);
diff --git a/test/Misc/ZeroLikeSized.C b/test/Misc/ZeroLikeSized.C
new file mode 100644
index 00000000..30f706bc
--- /dev/null
+++ b/test/Misc/ZeroLikeSized.C
@@ -0,0 +1,63 @@
+// RUN: %cladclang %s -I%S/../../include -o %t
+// RUN: %t | %filecheck_exec %s
+// Lock in the C++14 compatibility of the zero_like SFINAE.
+// RUN: %cladclang -std=c++14 %s -I%S/../../include -o %t14
+// RUN: %t14 | %filecheck_exec %s
+
+// A sized, size-constructible range (std::vector) builds its zero adjoint from
+// the size alone -- no copy of the primal's values -- while ranges without a
+// size-constructor (std::forward_list, std::array) fall back to copy-then-zero.
+// All must still yield an all-zero, same-shape result.
+
+#include "clad/Differentiator/Differentiator.h"
+#include "clad/Differentiator/STLBuiltins.h"
+
+#include <array>
+#include <cstdio>
+#include <forward_list>
+#include <vector>
+
+// Counts copies and does NOT value-initialize to zero: proves the sized path
+// avoids the primal copy yet still zeroes via zero_init.
+struct Counted {
+ double v = 7;
+ static long copies;
+ Counted() = default;
+ Counted(const Counted& o) : v(o.v) { ++copies; }
+ Counted& operator=(const Counted&) = default;
+};
+long Counted::copies = 0;
+
+namespace clad {
+void zero_init(Counted& c) { c.v = 0; }
+} // namespace clad
+
+template <class R> bool all_zero(const R& r) {
+ for (const auto& e : r)
+ if (e.v != 0)
+ return false;
+ return true;
+}
+
+int main() {
+ std::vector<Counted> vec(1000);
+ for (auto& c : vec)
+ c.v = 3.14;
+ Counted::copies = 0;
+ auto dvec = clad::zero_like(vec); // sized path: constructs, never copies
+ std::printf("%d %ld\n", (int)(all_zero(dvec) && dvec.size() == vec.size()),
+ Counted::copies);
+ // CHECK-EXEC: 1 0
+
+ std::forward_list<Counted> fl(3); // no size() -> copy fallback
+ for (auto& c : fl)
+ c.v = 2.0;
+ std::printf("%d\n", (int)all_zero(clad::zero_like(fl)));
+ // CHECK-EXEC: 1
+
+ std::array<Counted, 4> arr{}; // no size-ctor -> copy fallback
+ for (auto& c : arr)
+ c.v = 5.0;
+ std::printf("%d\n", (int)all_zero(clad::zero_like(arr)));
+ // CHECK-EXEC: 1
+}
--
2.39.5 (Apple Git-154)There was a problem hiding this comment.
I am concerned that this optimization is not generally correct. For example, for an std::vector<std::vector<double>> with an m x n (or ragged) shape, T(value.size()) preserves only the outer size. It constructs m empty inner vectors, and zero_init only zeros existing elements, so it cannot recover the inner sizes.
This particular case could be addressed by recursively applying the same construction to each element. In the longer term, however, that would still not make size-based construction safe as a generic rule. A size is not a complete structural descriptor: stateful allocators or memory resources, comparators, hash functions, key-equality objects, and other runtime metadata may be lost.
For example, although Clad does not appear to support differentiating std::unordered_map today, it is a concrete false positive for has_sized_ctor. It has an unordered_map(size_type bucket_count) constructor, so T(value.size()) constructs an empty container whose bucket count merely happens to equal the primal size. It drops all keys and values and resets the hash, equality, and allocator state.
Therefore, I think recursively reconstructing each element from the corresponding primal value can serve as the default zero_like strategy for sized ranges. This would preserve nested sizes without copying primal values. Types whose relevant structure cannot be fully reconstructed this way, such as those carrying stateful allocators or other runtime metadata, should provide type-specific zero_like overloads or explicitly opt into a more appropriate construction policy.
There was a problem hiding this comment.
That’s a fair point. However, I am really worried about the performance hit we are going to take with this feature. What are our alternative optimization options?
| template <class T, std::enable_if_t<std::is_arithmetic<T>::value || | ||
| std::is_enum<T>::value, | ||
| int> = 0> | ||
| CUDA_HOST_DEVICE T zero_like(const T&) { |
There was a problem hiding this comment.
Two customization points now coexist (zero_init in-place vs zero_like return-new) and the boundary is undocumented. Add to the doc-comment + docs/: "zero_like(x) returns a value structurally like x with zero_init applied; override it when copy-then-zero is wrong — owning storage, refcounts, device memory." Consider documenting zero_like as the adjoint-construction extension point and zero_init as the primitive it builds on.
We can add the missing section on zero_init, too.
| @@ -0,0 +1,54 @@ | |||
| // RUN: %cladclang %s -I%S/../../include -o %t 2>&1 | %filecheck %s | |||
There was a problem hiding this comment.
zero_like's SFINAE is deliberately C++14-safe (the NOLINT(modernize-type-traits)), but these run one mode. Add -std=c++14 and -std=c++20 RUN lines so a future _v-trait/if constexpr creep can't silently regress C++14 clients.
Same holds for the other test.
|
clang-tidy review says "All clean, LGTM! 👍" |
While investigating the following case:
I found that even though
valuesis not selected as an independent parameter, Clad still needs an internal_d_valuesto propagate the derivative through the assignment.For
values = {10.0}andx = 3.0, the expected derivative is6.0. However,_d_valueswas initialized by copying the primal value and was never cleared, so its initial value leaked into the reverse pass and produced16.0.This means that every internal adjoint created by Clad, including adjoints for non-independent parameters, must represent a valid zero tangent before the reverse pass starts.
While fixing this, I found that the current API does not have enough expressive power to construct such an adjoint correctly for every type.
For a record parameter of type
T, the existing logic is roughly:Neither option is generally correct.
For example, libc++ implements
std::vectorusing direct pointer fields. The field-based check therefore selects:std::vector<double> _d_values{};but this loses the vector’s runtime structure:
_d_valuesis empty whilevaluesmay contain several elements.At the same time, simply ignoring pointer fields and always copying is also unsafe. A user-defined type may contain an internal pointer referring to its own storage, and its copy constructor may leave the copied pointer referring to the primal object.
In other words, field layout alone cannot tell us whether copying a type creates a valid, independently owned adjoint.
clad::zero_initcannot fully solve this either. Its current purpose is to reset an already existing adjoint while preserving its structure. It is also used for arrays, loop resets, and user-provided specializations. Changing it into an adjoint-construction operation would change its existing semantics and break backward compatibility.This PR therefore introduces a separate customization point:
auto adjoint = clad::zero_like(primal);The intended contract of
zero_likeis:Types can provide their own overload:
For
std::vector, the implementation copies the vector to obtain independent storage and preserve its size, then applieszero_initto its elements:A Tensor backend can similarly implement this by delegating to an operation such as
at::zeros_like.When Clad creates an internal adjoint from an existing primal value, it now tries a viable
clad::zero_likeoverload first. If no overload is available, it preserves the previous copy/value-initialization logic as a backward-compatible fallback.This also removes the need for a
std::vector-specific exception inReverseModeVisitor: knowledge about how to construct a vector adjoint now lives inSTLBuiltins.h, where it belongs.Finally, this API provides an important building block for improving
clad::gradient. Currently, users must allocate zero adjoints themselves and pass_d_xxxpointers toexecute. Withzero_like, Clad can eventually construct those terminal adjoints from the primal arguments and return them directly:auto gradients = gradient.execute(args...);This PR does not change that interface yet, but it provides the missing construction operation needed to implement it correctly for vectors, tensors, and user-defined structured types.