Skip to content

Commit 6e3b00f

Browse files
committed
Guard the zero_init memcpy fallback in host builds too.
clad::zero_init falls back to zeroing an object with a byte-wise memcpy, which is not well-defined for every type. The static_assert guarding it was compiled only into CUDA translation units, so the same call in an ordinary host build was unguarded. That gap is reachable from a direct clad::zero_init call: clad's own codegen routes only arrays and VLAs here, so it is user-written calls -- such as the one the CUDA test makes -- that went unchecked on the host. Compile the guard unconditionally, and skip instantiating the fill once it has failed, so a rejected type is not memcpy'd anyway. CUDA translation units should be unaffected, as __CUDACC__ is defined for both their host and device passes and the assert was already active there; that part is reasoned rather than run, since it needs a GPU, so CI is what confirms it. This can break a host build that previously compiled: zeroing a non-trivially-destructible type was undefined but silent, and is now an error. The assert also keeps its existing condition and message, both weaker than the situation warrants -- trivial destructibility is less than the memcpy requires, so a type with a user copy constructor and a trivial destructor still passes, and the message still says "device fallback" though it now fires on the host. Tightening either changes the test that pins the message, and is left for when clad actually routes adjoints of such types through here.
1 parent b865c6f commit 6e3b00f

2 files changed

Lines changed: 92 additions & 23 deletions

File tree

include/clad/Differentiator/Differentiator.h

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -238,34 +238,36 @@ CUDA_HOST_DEVICE auto back(TapeType& of) -> decltype(of.back()) {
238238
template <class T,
239239
typename std::enable_if<!is_range<T>::value, int>::type = 0>
240240
CUDA_HOST_DEVICE void zero_impl(volatile T& t) {
241-
#if defined(__CUDACC__)
242-
static_assert(std::is_trivially_destructible<T>::value,
243-
"Clad device fallback zero_init requires trivially "
244-
"destructible types.");
245-
#endif
246-
// Fill an array with zeros.
247-
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)
248-
unsigned char tmp[sizeof(T)] = {};
241+
// Bound once so the assertion and the guard below cannot drift apart: a
242+
// type the assertion rejects must not go on to be memcpy'd anyway.
243+
constexpr bool is_zeroable = std::is_trivially_destructible<T>::value;
244+
static_assert(is_zeroable, "Clad device fallback zero_init requires "
245+
"trivially destructible types.");
246+
if constexpr (is_zeroable) {
247+
// Fill an array with zeros.
248+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)
249+
unsigned char tmp[sizeof(T)] = {};
249250

250251
#if __has_builtin(__builtin_memcpy)
251-
__builtin_memcpy(const_cast<T*>(&t), tmp, sizeof(T));
252+
__builtin_memcpy(const_cast<T*>(&t), tmp, sizeof(T));
252253
#elif defined(__CUDACC__)
253-
// Fallback for the devices that don't have __builtin_memcpy.
254-
// Transfers the zero with a loop. Unlike memcpyt, this does not create the
255-
// object in the destination region of storage and language semantics can't
256-
// be fully preserved
257-
volatile unsigned char* byte_ptr =
258-
reinterpret_cast<volatile unsigned char*>(const_cast<T*>(&t));
259-
for (std::size_t i = 0; i < sizeof(T); ++i)
260-
byte_ptr[i] = 0;
254+
// Fallback for the devices that don't have __builtin_memcpy.
255+
// Transfers the zero with a loop. Unlike memcpyt, this does not create
256+
// the object in the destination region of storage and language semantics
257+
// can't be fully preserved
258+
volatile unsigned char* byte_ptr =
259+
reinterpret_cast<volatile unsigned char*>(const_cast<T*>(&t));
260+
for (std::size_t i = 0; i < sizeof(T); ++i)
261+
byte_ptr[i] = 0;
261262
#else
262-
// Transfer the zeros with the magic function memcpy which can implicitly
263-
// create objects in the destination region of storage immediately prior to
264-
// copying the sequence of characters to the destination [27.5.1(3)].
265-
// (C++ has deprecated the volatile qualifiers. However, we drop them here
266-
// to make sure things still work with codebases which still have them)
267-
std::memcpy(const_cast<T*>(&t), tmp, sizeof(T));
263+
// Transfer the zeros with the magic function memcpy which can implicitly
264+
// create objects in the destination region of storage immediately prior
265+
// to copying the sequence of characters to the destination [27.5.1(3)].
266+
// (C++ has deprecated the volatile qualifiers. However, we drop them here
267+
// to make sure things still work with codebases which still have them)
268+
std::memcpy(const_cast<T*>(&t), tmp, sizeof(T));
268269
#endif
270+
}
269271
}
270272

271273
template <class T, typename std::enable_if<is_range<T>::value, int>::type = 0>

test/Misc/ZeroInitViability.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// RUN: %cladclang %s -I%S/../../include -fsyntax-only -Xclang -verify
2+
//
3+
// Nothing is ignored: every diagnostic is spelled out below. In particular a
4+
// -Wnontrivial-* warning must not appear, as it would mean the rejected type
5+
// still reaches the memcpy, which is the thing being prevented.
6+
7+
// clad::zero_init's fallback zeroes an object with a byte-wise memcpy, which is
8+
// not well-defined for every type. Its guard used to be compiled only into CUDA
9+
// translation units, so a direct clad::zero_init call in an ordinary host build
10+
// went unchecked. Check that a host build is guarded too.
11+
12+
#include "clad/Differentiator/Differentiator.h"
13+
14+
#include <vector>
15+
16+
struct TrivialPod {
17+
double a, b;
18+
};
19+
20+
struct WithCtorTrivialCopy { // a user ctor does not stop trivial copyability
21+
double a, b;
22+
WithCtorTrivialCopy(double x, double y) : a(x), b(y) {}
23+
};
24+
25+
struct Owning { // owns a heap handle: memcpy-zeroing it would corrupt it
26+
double* p;
27+
Owning() : p(new double(0)) {}
28+
Owning(const Owning& o) : p(new double(*o.p)) {}
29+
~Owning() { delete p; }
30+
};
31+
32+
struct HasOverload {
33+
double* p;
34+
};
35+
namespace clad {
36+
inline void zero_init(HasOverload& h) { *h.p = 0; }
37+
} // namespace clad
38+
39+
void accepted() {
40+
double d = 1;
41+
clad::zero_init(d);
42+
43+
TrivialPod p{1, 2};
44+
clad::zero_init(p);
45+
46+
WithCtorTrivialCopy w(1, 2);
47+
clad::zero_init(w);
48+
49+
std::vector<double> v(3);
50+
clad::zero_init(v);
51+
52+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)
53+
double arr[2]{}; // a C array is iterable, and is what clad zeroes in a loop
54+
clad::zero_init(arr);
55+
56+
HasOverload h{&d}; // an overload still wins over the fallback
57+
clad::zero_init(h);
58+
}
59+
60+
void rejected() {
61+
Owning o;
62+
// expected-error@clad/Differentiator/Differentiator.h:* {{Clad device fallback zero_init requires trivially destructible types}}
63+
// expected-note@clad/Differentiator/Differentiator.h:* {{in instantiation of function template specialization 'clad::zero_impl<Owning, 0>' requested here}}
64+
clad::zero_init(o); // expected-note {{in instantiation of function template specialization 'clad::zero_init<Owning>' requested here}}
65+
}
66+
67+
int main() { return 0; }

0 commit comments

Comments
 (0)