Why does order of defining hidden friend binary operator- vs operator==, and using int vs concepts matter to NVCC?

16 hours ago 1
ARTICLE AD BOX

I'm trying to create a pointer like type wrapper for CUDA device pointers, but ran into a problem I can't reproduce with other compilers in regular MSVC or GCC. Basically, depending on the order I define operator- which I define as a hidden friend like so:

[[nodiscard]] friend TestPointerLike operator-(TestPointerLike lhs, std::integral auto n) noexcept { // lhs -= n; removed for now for simplicity in example, since only testing compilation return lhs; }

Depending on if I define it before operator == with std::nullopt_t, or after, or if I use int, or std::integral auto for n and I make it a non-hidden friend and just define it outside the class definition it may or may not compile the == operator. Why is this the case?

The following is the minimal example (there are four tests, with the final one, TestPointerLike4, being the failing case).

Godbolt link also here (using NVCC): https://godbolt.org/z/cWsP3zoEj Godbolt link to it working in MSVC and GCC https://godbolt.org/z/a7EKr5hb9

#include <type_traits> #include <concepts> #include <iostream> template<typename T> struct TestPointerLike { private: T *m_data = nullptr; public: [[nodiscard]] T *get() noexcept { return m_data; } [[nodiscard]] const T *get() const { return m_data; } TestPointerLike() noexcept = default; explicit constexpr TestPointerLike(std::nullptr_t) noexcept : TestPointerLike() {}; [[nodiscard]] friend bool operator==(TestPointerLike dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() == nullptr; } [[nodiscard]] friend bool operator!=(TestPointerLike dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() != nullptr; } [[nodiscard]] friend TestPointerLike operator-(TestPointerLike lhs, std::integral auto n) noexcept { // lhs -= n; return lhs; } }; template<typename T> struct TestPointerLike2 { private: T *m_data = nullptr; public: [[nodiscard]] T *get() noexcept { return m_data; } [[nodiscard]] const T *get() const { return m_data; } TestPointerLike2() noexcept = default; explicit constexpr TestPointerLike2(std::nullptr_t) noexcept : TestPointerLike2() {}; [[nodiscard]] friend TestPointerLike2 operator-(TestPointerLike2 lhs, int n) noexcept { // lhs -= n; return lhs; } [[nodiscard]] friend bool operator==(TestPointerLike2 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() == nullptr; } [[nodiscard]] friend bool operator!=(TestPointerLike2 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() != nullptr; } }; template<typename T> struct TestPointerLike3 { private: T *m_data = nullptr; public: [[nodiscard]] T *get() noexcept { return m_data; } [[nodiscard]] const T *get() const { return m_data; } TestPointerLike3() noexcept = default; explicit constexpr TestPointerLike3(std::nullptr_t) noexcept : TestPointerLike3() {}; friend TestPointerLike3 operator-(TestPointerLike3 lhs, std::integral auto n) noexcept; [[nodiscard]] friend bool operator==(TestPointerLike3 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() == nullptr; } [[nodiscard]] friend bool operator!=(TestPointerLike3 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() != nullptr; } }; template<typename T> [[nodiscard]] TestPointerLike3<T> operator-(TestPointerLike3<T> lhs, std::integral auto n) noexcept { // lhs -= n; return lhs; } template<typename T> struct TestPointerLike4 { private: T *m_data = nullptr; public: [[nodiscard]] T *get() noexcept { return m_data; } [[nodiscard]] const T *get() const { return m_data; } TestPointerLike4() noexcept = default; explicit constexpr TestPointerLike4(std::nullptr_t) noexcept : TestPointerLike4() {}; [[nodiscard]] friend TestPointerLike4 operator-(TestPointerLike4 lhs, std::integral auto n) noexcept { // lhs -= n; return lhs; } [[nodiscard]] friend bool operator==(TestPointerLike4 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() == nullptr; } [[nodiscard]] friend bool operator!=(TestPointerLike4 dev_ptr, std::nullptr_t) noexcept{ return dev_ptr.get() != nullptr; } }; int main() { { TestPointerLike<double> f64_ptr; bool bool_test = f64_ptr == nullptr; } { TestPointerLike2<double> f64_ptr; bool bool_test = f64_ptr == nullptr; } { TestPointerLike3<double> f64_ptr; bool bool_test = f64_ptr == nullptr; } { TestPointerLike4<double> f64_ptr; bool bool_test = f64_ptr == nullptr; } }

the error I get is:

<source>(187): error: no operator "==" matches these operands operand types are: TestPointerLike4<double> == std::nullptr_t bool bool_test = f64_ptr == nullptr; ^ <source>(187): note #3328-D: built-in operator==(<promoted arithmetic>, <promoted arithmetic>) does not match because argument #1 does not match parameter bool bool_test = f64_ptr == nullptr; ^ <source>(187): note #3328-D: built-in operator==(<nullptr>, <nullptr>) does not match because argument #1 does not match parameter bool bool_test = f64_ptr == nullptr; ^ 1 error detected in the compilation of "<source>". Compiler returned: 2
Read Entire Article