device_raw_ptr
is a simple fancy pointer. It essentially wrap pointers to GPU memory. It's sole purpose is to separate out host pointers from device pointers, i.e. they should not be inter-convertible and must not be dereferenced. At the same time, they should be zero-cost (with respect to raw host pointers) and be maximally compatible with regular pointers.
Helper class:
template <class T>
struct equality_operators {
/*
** The deriving class must implement the following:
** friend bool operator==(const T&, const T&);
*/
friend bool operator!=(const T& lhs, const T& rhs) { return !static_cast<bool>(lhs == rhs); }
};
template <class T>
struct less_than_operators {
/*
** The deriving class must implement the following:
** friend bool operator<(const T&, const T&);
*/
friend bool operator>(const T& lhs, const T& rhs) { return rhs < lhs; }
friend bool operator<=(const T& lhs, const T& rhs) { return !static_cast<bool>(lhs > rhs); }
friend bool operator>=(const T& lhs, const T& rhs) { return !static_cast<bool>(lhs < rhs); }
};
template <class T>
struct relational_operators : equality_operators<T>, less_than_operators<T> { };
device_raw_ptr
implementation:
template <class T>
class device_raw_ptr : public relational_operators<T> {
static_assert(std::is_standard_layout<T>::value, "T must satisfy StandardLayoutType");
public:
using element_type = std::remove_extent_t<T>;
constexpr device_raw_ptr() noexcept = default;
constexpr device_raw_ptr(std::nullptr_t) noexcept : ptr { nullptr } { }
constexpr device_raw_ptr(const device_raw_ptr& other) noexcept = default;
explicit device_raw_ptr(element_type* ptr_) noexcept : ptr{ ptr_ } { }
device_raw_ptr(device_raw_ptr&& other) noexcept : ptr{ other.ptr } { other.reset(); }
device_raw_ptr& operator=(const device_raw_ptr& other) noexcept {
swap(device_raw_ptr(other), *this);
return *this;
}
device_raw_ptr& operator=(device_raw_ptr&& other) noexcept {
swap(device_raw_ptr(other), *this);
return *this;
}
void reset() noexcept { ptr = nullptr; }
void reset(T* ptr_) noexcept { ptr = ptr_; }
element_type* get() noexcept { return ptr; };
const element_type* get() const noexcept { return ptr; }
friend void swap(device_raw_ptr& lhs, device_raw_ptr& rhs) noexcept {
using std::swap;
std::swap(lhs.ptr, rhs.ptr);
}
explicit operator bool() const noexcept { return static_cast<bool>(ptr); }
device_raw_ptr& operator++() noexcept {
++ptr;
return *this;
}
device_raw_ptr operator++(int) noexcept {
device_raw_ptr tmp(*this);
ptr++;
return tmp;
}
device_raw_ptr& operator+=(std::ptrdiff_t offset) noexcept {
ptr += offset;
return *this;
}
device_raw_ptr& operator-=(std::ptrdiff_t offset) noexcept {
ptr -= offset;
return *this;
}
friend device_raw_ptr& operator+(device_raw_ptr lhs, std::ptrdiff_t offset) noexcept {
lhs += offset;
return lhs;
}
friend device_raw_ptr& operator-(device_raw_ptr lhs, std::ptrdiff_t offset) noexcept {
lhs -= offset;
return lhs;
}
/* required by relational_operators base class */
friend bool operator==(const device_raw_ptr& lhs, const device_raw_ptr& rhs) noexcept { return lhs.ptr == rhs.ptr; }
friend bool operator<(const device_raw_ptr& lhs, const device_raw_ptr& rhs) noexcept { return lhs.ptr < rhs.ptr; }
protected:
T *ptr;
};
template <class T, class U, class V>
std::basic_ostream<U, V>& operator<<(std::basic_ostream<U, V>& os, const device_raw_ptr<T>& other) {
os << other.get() << " (device)";
return os;
}
I am also looking for suggestions on how to order different things inside a class.
Might it be better to initialize with nullptr
instead of the default constructor. This breaks compatibility but I think it might be worth considering the compiler would mostly optimize the assignment if it's immediately overwritten.
swap
which accepts non-const lvalue as arguments. You can find it in the copy & move constructor. \$\endgroup\$relational_operator
inheritance. \$\endgroup\$