Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADT] Specialize ValueIsPresent for PointerUnion #121847

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

s-barannikov
Copy link
Contributor

@s-barannikov s-barannikov commented Jan 6, 2025

Two instances of PointerUnion with different active members and null value compare unequal. Currently, this results in counterintuitive behavior when using functions from Casting.h, e.g.:

  PointerUnion<int *, float *> U;
  // U = (int *)nullptr;
  dyn_cast<int *>(U); // Aborts
  dyn_cast<float *>(U); // Aborts
  U = (float *)nullptr;
  dyn_cast<int *>(U); // OK
  dyn_cast<float *>(U); // OK

dyn_cast should abort in all cases because the argument is null. Currently, it aborts only if the first member is active. This happens because the partial template specialization of ValueIsPresent for nullable types compares the union with a union constructed from nullptr, and the two unions compare equal only if their active members are the same.

This patch specializes ValueIsPresent further to make isPresent() return false for all possible null values, and fixes two places where the old behavior was exploited.

Currently, two instances of `PointerUnion` with different active members
and null value compare unequal. In some cases, this results in
counterintuitive behavior when using functions from `Casting.h`, e.g.:

```
  PointerUnion<int *, float *> U;
  // U = (int *)nullptr;
  dyn_cast<int *>(U); // Aborts
  dyn_cast<float *>(U); // Aborts
  U = (float *)nullptr;
  dyn_cast<int *>(U); // OK
  dyn_cast<float *>(U); // OK
```

`dyn_cast` should abort in all cases because the argument is null.
Currently, it aborts only if the first member is active. This happens
because the partial template specialization of `ValueIsPresent` for
nullable types compares the union with a union constructed from nullptr,
and the two unions compare equal only if their active members are the
same.

This patch makes two instances of a union compare equal if they are both
null regardless of their active members, and fixes two places where the
old behavior was exploited.
@llvmbot
Copy link
Member

llvmbot commented Jan 6, 2025

@llvm/pr-subscribers-llvm-support
@llvm/pr-subscribers-llvm-adt

@llvm/pr-subscribers-backend-amdgpu

Author: Sergei Barannikov (s-barannikov)

Changes

Currently, two instances of PointerUnion with different active members and null value compare unequal. This may result in counterintuitive behavior when using functions from Casting.h, e.g.:

  PointerUnion&lt;int *, float *&gt; U;
  // U = (int *)nullptr;
  dyn_cast&lt;int *&gt;(U); // Aborts
  dyn_cast&lt;float *&gt;(U); // Aborts
  U = (float *)nullptr;
  dyn_cast&lt;int *&gt;(U); // OK
  dyn_cast&lt;float *&gt;(U); // OK

dyn_cast should abort in all cases because the argument is null. Currently, it aborts only if the first member is active. This happens because the partial template specialization of ValueIsPresent for nullable types compares the union with a union constructed from nullptr, and the two unions compare equal only if their active members are the same.

This patch makes two instances of a union compare equal if they are both null regardless of their active members, and fixes two places where the old behavior was exploited.


Full diff: https://github.com/llvm/llvm-project/pull/121847.diff

4 Files Affected:

  • (modified) llvm/include/llvm/ADT/PointerUnion.h (+6-6)
  • (modified) llvm/lib/CodeGen/RegisterBankInfo.cpp (+2-2)
  • (modified) llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp (+2-2)
  • (modified) llvm/unittests/ADT/PointerUnionTest.cpp (+10-3)
diff --git a/llvm/include/llvm/ADT/PointerUnion.h b/llvm/include/llvm/ADT/PointerUnion.h
index 7d4ed02b622626..bea9d6a4569b0b 100644
--- a/llvm/include/llvm/ADT/PointerUnion.h
+++ b/llvm/include/llvm/ADT/PointerUnion.h
@@ -198,14 +198,14 @@ class PointerUnion
   }
 };
 
-template <typename ...PTs>
-bool operator==(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
-  return lhs.getOpaqueValue() == rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator==(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+  return (!LHS && !RHS) || LHS.getOpaqueValue() == RHS.getOpaqueValue();
 }
 
-template <typename ...PTs>
-bool operator!=(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
-  return lhs.getOpaqueValue() != rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator!=(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+  return !operator==(LHS, RHS);
 }
 
 template <typename ...PTs>
diff --git a/llvm/lib/CodeGen/RegisterBankInfo.cpp b/llvm/lib/CodeGen/RegisterBankInfo.cpp
index e1720b038e2361..5a8cf13ad11fd5 100644
--- a/llvm/lib/CodeGen/RegisterBankInfo.cpp
+++ b/llvm/lib/CodeGen/RegisterBankInfo.cpp
@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister(
 
   // If the register already has a class, fallback to MRI::constrainRegClass.
   auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg);
-  if (isa<const TargetRegisterClass *>(RegClassOrBank))
+  if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank))
     return MRI.constrainRegClass(Reg, &RC);
 
-  const RegisterBank *RB = cast<const RegisterBank *>(RegClassOrBank);
+  const auto *RB = dyn_cast_if_present<const RegisterBank *>(RegClassOrBank);
   // Otherwise, all we can do is ensure the bank covers the class, and set it.
   if (RB && !RB->covers(RC))
     return nullptr;
diff --git a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
index 704435dad65d7b..8fa656c77e90ed 100644
--- a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
@@ -3708,10 +3708,10 @@ const TargetRegisterClass *
 SIRegisterInfo::getConstrainedRegClassForOperand(const MachineOperand &MO,
                                          const MachineRegisterInfo &MRI) const {
   const RegClassOrRegBank &RCOrRB = MRI.getRegClassOrRegBank(MO.getReg());
-  if (const RegisterBank *RB = dyn_cast<const RegisterBank *>(RCOrRB))
+  if (const auto *RB = dyn_cast_if_present<const RegisterBank *>(RCOrRB))
     return getRegClassForTypeOnBank(MRI.getType(MO.getReg()), *RB);
 
-  if (const auto *RC = dyn_cast<const TargetRegisterClass *>(RCOrRB))
+  if (const auto *RC = dyn_cast_if_present<const TargetRegisterClass *>(RCOrRB))
     return getAllocatableClass(RC);
 
   return nullptr;
diff --git a/llvm/unittests/ADT/PointerUnionTest.cpp b/llvm/unittests/ADT/PointerUnionTest.cpp
index acddb789601494..d870339b01397d 100644
--- a/llvm/unittests/ADT/PointerUnionTest.cpp
+++ b/llvm/unittests/ADT/PointerUnionTest.cpp
@@ -53,9 +53,16 @@ TEST_F(PointerUnionTest, Comparison) {
   EXPECT_TRUE(i4 != l4);
   EXPECT_TRUE(f4 != l4);
   EXPECT_TRUE(l4 != d4);
-  EXPECT_TRUE(i4null != f4null);
-  EXPECT_TRUE(i4null != l4null);
-  EXPECT_TRUE(i4null != d4null);
+  EXPECT_TRUE(i4null == f4null);
+  EXPECT_FALSE(i4null != f4null);
+  EXPECT_TRUE(i4null == l4null);
+  EXPECT_FALSE(i4null != l4null);
+  EXPECT_TRUE(i4null == d4null);
+  EXPECT_FALSE(i4null != d4null);
+  EXPECT_FALSE(i4null == i4);
+  EXPECT_TRUE(i4null != i4);
+  EXPECT_FALSE(i4null == f4);
+  EXPECT_TRUE(i4null != f4);
 }
 
 TEST_F(PointerUnionTest, Null) {

@llvmbot
Copy link
Member

llvmbot commented Jan 6, 2025

@llvm/pr-subscribers-llvm-regalloc

Author: Sergei Barannikov (s-barannikov)

Changes

Currently, two instances of PointerUnion with different active members and null value compare unequal. This may result in counterintuitive behavior when using functions from Casting.h, e.g.:

  PointerUnion&lt;int *, float *&gt; U;
  // U = (int *)nullptr;
  dyn_cast&lt;int *&gt;(U); // Aborts
  dyn_cast&lt;float *&gt;(U); // Aborts
  U = (float *)nullptr;
  dyn_cast&lt;int *&gt;(U); // OK
  dyn_cast&lt;float *&gt;(U); // OK

dyn_cast should abort in all cases because the argument is null. Currently, it aborts only if the first member is active. This happens because the partial template specialization of ValueIsPresent for nullable types compares the union with a union constructed from nullptr, and the two unions compare equal only if their active members are the same.

This patch makes two instances of a union compare equal if they are both null regardless of their active members, and fixes two places where the old behavior was exploited.


Full diff: https://github.com/llvm/llvm-project/pull/121847.diff

4 Files Affected:

  • (modified) llvm/include/llvm/ADT/PointerUnion.h (+6-6)
  • (modified) llvm/lib/CodeGen/RegisterBankInfo.cpp (+2-2)
  • (modified) llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp (+2-2)
  • (modified) llvm/unittests/ADT/PointerUnionTest.cpp (+10-3)
diff --git a/llvm/include/llvm/ADT/PointerUnion.h b/llvm/include/llvm/ADT/PointerUnion.h
index 7d4ed02b622626..bea9d6a4569b0b 100644
--- a/llvm/include/llvm/ADT/PointerUnion.h
+++ b/llvm/include/llvm/ADT/PointerUnion.h
@@ -198,14 +198,14 @@ class PointerUnion
   }
 };
 
-template <typename ...PTs>
-bool operator==(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
-  return lhs.getOpaqueValue() == rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator==(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+  return (!LHS && !RHS) || LHS.getOpaqueValue() == RHS.getOpaqueValue();
 }
 
-template <typename ...PTs>
-bool operator!=(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
-  return lhs.getOpaqueValue() != rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator!=(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+  return !operator==(LHS, RHS);
 }
 
 template <typename ...PTs>
diff --git a/llvm/lib/CodeGen/RegisterBankInfo.cpp b/llvm/lib/CodeGen/RegisterBankInfo.cpp
index e1720b038e2361..5a8cf13ad11fd5 100644
--- a/llvm/lib/CodeGen/RegisterBankInfo.cpp
+++ b/llvm/lib/CodeGen/RegisterBankInfo.cpp
@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister(
 
   // If the register already has a class, fallback to MRI::constrainRegClass.
   auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg);
-  if (isa<const TargetRegisterClass *>(RegClassOrBank))
+  if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank))
     return MRI.constrainRegClass(Reg, &RC);
 
-  const RegisterBank *RB = cast<const RegisterBank *>(RegClassOrBank);
+  const auto *RB = dyn_cast_if_present<const RegisterBank *>(RegClassOrBank);
   // Otherwise, all we can do is ensure the bank covers the class, and set it.
   if (RB && !RB->covers(RC))
     return nullptr;
diff --git a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
index 704435dad65d7b..8fa656c77e90ed 100644
--- a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
@@ -3708,10 +3708,10 @@ const TargetRegisterClass *
 SIRegisterInfo::getConstrainedRegClassForOperand(const MachineOperand &MO,
                                          const MachineRegisterInfo &MRI) const {
   const RegClassOrRegBank &RCOrRB = MRI.getRegClassOrRegBank(MO.getReg());
-  if (const RegisterBank *RB = dyn_cast<const RegisterBank *>(RCOrRB))
+  if (const auto *RB = dyn_cast_if_present<const RegisterBank *>(RCOrRB))
     return getRegClassForTypeOnBank(MRI.getType(MO.getReg()), *RB);
 
-  if (const auto *RC = dyn_cast<const TargetRegisterClass *>(RCOrRB))
+  if (const auto *RC = dyn_cast_if_present<const TargetRegisterClass *>(RCOrRB))
     return getAllocatableClass(RC);
 
   return nullptr;
diff --git a/llvm/unittests/ADT/PointerUnionTest.cpp b/llvm/unittests/ADT/PointerUnionTest.cpp
index acddb789601494..d870339b01397d 100644
--- a/llvm/unittests/ADT/PointerUnionTest.cpp
+++ b/llvm/unittests/ADT/PointerUnionTest.cpp
@@ -53,9 +53,16 @@ TEST_F(PointerUnionTest, Comparison) {
   EXPECT_TRUE(i4 != l4);
   EXPECT_TRUE(f4 != l4);
   EXPECT_TRUE(l4 != d4);
-  EXPECT_TRUE(i4null != f4null);
-  EXPECT_TRUE(i4null != l4null);
-  EXPECT_TRUE(i4null != d4null);
+  EXPECT_TRUE(i4null == f4null);
+  EXPECT_FALSE(i4null != f4null);
+  EXPECT_TRUE(i4null == l4null);
+  EXPECT_FALSE(i4null != l4null);
+  EXPECT_TRUE(i4null == d4null);
+  EXPECT_FALSE(i4null != d4null);
+  EXPECT_FALSE(i4null == i4);
+  EXPECT_TRUE(i4null != i4);
+  EXPECT_FALSE(i4null == f4);
+  EXPECT_TRUE(i4null != f4);
 }
 
 TEST_F(PointerUnionTest, Null) {

@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister(

// If the register already has a class, fallback to MRI::constrainRegClass.
auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg);
if (isa<const TargetRegisterClass *>(RegClassOrBank))
if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think either of these 2 instances should ever encounter a register without a set class or bank, this is papering over a different bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input gMIR to instruction selector shouldn't contain registers without class/bank.
Such registers are created during instruction selection if an imported SelectionDAG pattern contains several instructions in the "destination DAG" of the pattern:

def : GCNPat <
  (UniformUnaryFrag<fabs> (v2f16 SReg_32:$src)),
  (S_AND_B32 SReg_32:$src, (S_MOV_B32 (i32 0x7fff7fff)))
>;

This is what -gen-global-isel generates for this pattern:

        GIR_MakeTempReg, /*TempRegID*/0, /*TypeID*/GILLT_s1,
        GIR_BuildMI, /*InsnID*/1, /*Opcode*/GIMT_Encode2(AMDGPU::S_MOV_B32),
        GIR_AddTempRegister, /*InsnID*/1, /*TempRegID*/0, /*TempRegFlags*/GIMT_Encode2(RegState::Define),
        ...
        GIR_ConstrainSelectedInstOperands, /*InsnID*/1,
        GIR_BuildRootMI, /*Opcode*/GIMT_Encode2(AMDGPU::S_AND_B32),
        ...
        GIR_AddSimpleTempRegister, /*InsnID*/0, /*TempRegID*/0,
        GIR_RootConstrainSelectedInstOperands,

GIR_MakeTempReg creates a register without class/bank for the result of the S_MOV_B32. The register gets its class when executing GIR_ConstrainSelectedInstOperands action, which calls this function, which calls MRI.setRegClass() at the end.

I don't know if this should be considered a bug. If it should, I can try to address it separately (probably in #121270).


(Unrelated to this PR). Note that the type of the temporary register is s1. It is chosen arbitrarily.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GIR_MakeTempReg creates a register without class/bank for the result of the S_MOV_B32.

As I mentioned in the other PR this is broken. In no context should an incomplete virtual register be used by an instruction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears there is another context when class/bank may not be set: 7e1f66d

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After regbankselect / in the selection pass, there must be a class or bank set. The null/null case is only valid before that, when a generic vreg must have a type

Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not really convinced this change is right. If PointerUnion allows having null pointers of different types, then treating them as equal may not be correct in general.

I think the fix here should either be that assigning null pointer that is not nullptr_t to a PointerUnion is invalid, or that dyn_cast_if_present should account for the different null pointers.

@nikic nikic requested review from kuhar and dwblaikie January 7, 2025 14:17
Copy link
Member

@kuhar kuhar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unusual / unexpected to me, Do we have an example of similar semantics in some other LLVM type or an external library?

Hm, I'm not really convinced this change is right. If PointerUnion allows having null pointers of different types, then treating them as equal may not be correct in general.

+1

@s-barannikov
Copy link
Contributor Author

Hm, I'm not really convinced this change is right. If PointerUnion allows having null pointers of different types, then treating them as equal may not be correct in general.

I think I can agree with that. This makes sense if you think of PointerUnion as std::variant, which compares unequal if the compared objects have different active members.

I think the fix here should either be that assigning null pointer that is not nullptr_t to a PointerUnion is invalid,

The assignment happens here. It does not call an assignment operator, it constructs a PointerUnion from the specified (typed) null pointer and then calls copy constructor. If we want to prohibit this, we need to add a dynamic check to the constructor accepting a typed pointer. I can reporpose this PR to do that.

or that dyn_cast_if_present should account for the different null pointers.

I tried to specialize ValueIsPresent for PointerUnion, but either this is not possible or I don't know how to do that properly. Whatever I tried resulted in "ambiguous partial specialization" errors. Here is the generic specialization for nullable types:

static inline bool isPresent(const T &t) { return t != T(nullptr); }

which needs to be changed to something like:

  static inline bool isPresent(const T &t) { return static_cast<bool>(t); }

@s-barannikov
Copy link
Contributor Author

Do we have an example of similar semantics in some other LLVM type or an external library?

std::optional with polymorphic type comes to mind.

EXPECT_FALSE(isa_and_present<int *>(i4null));
EXPECT_FALSE(isa_and_present<float *>(f4null));
EXPECT_FALSE(isa_and_present<long long *>(l4null));
EXPECT_FALSE(isa_and_present<double *>(d4null));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, only the first isa_and_present returned false.

@s-barannikov s-barannikov changed the title [ADT] Make null PointerUnion with different active members compare equal [ADT] Specialize ValueIsPresent for PointerUnion Jan 8, 2025
@s-barannikov s-barannikov requested review from nikic and kuhar January 8, 2025 00:32
Copy link
Member

@kuhar kuhar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much better to me than trying to relax equality comparisons

llvm/include/llvm/ADT/PointerUnion.h Outdated Show resolved Hide resolved
// Override the default behavior to return false for all possible null values.
template <typename... PTs>
struct ValueIsPresent<PointerUnion<PTs...>,
std::enable_if_t<IsNullable<PointerUnion<PTs...>>>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enable_if needed? Isn't PointerUnion always nullable because it satisfies std::is_constructible_v<PointerUnion<PTs...>, std::nullptr_t>?

Copy link
Contributor Author

@s-barannikov s-barannikov Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this looks suspicious, but this is the only way I could get it compiled without errors.
I would appreciate it if someone could explain to me why this is necessary and how it can be simplified.

https://godbolt.org/z/x6dKhr5x1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the underlying issue is that without this, the main partial specialization from here would be as good of a match as this one:

static inline bool isPresent(const T &t) { return t != T(nullptr); }
. IE I don't think the exact predicate used in this enable_if matters as long as it evaluates to void at the end both L622 and here. In the godbolt link, enable_if<true> doesn't work because it appears in non-deduced context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way to handle this would be to change the main ValueIsPresent for nullable types to cast to bool instead, or introduce some traits for nullable types that would tell you how to check for null values that you can specialize for PointerUnion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IE I don't think the exact predicate used in this enable_if matters as long as it evaluates to void at the end both L622 and here. In the godbolt link, enable_if<true> doesn't work because it appears in non-deduced context.

I tried

template <typename... PTs>
struct ValueIsPresent<PointerUnion<PTs...>, std::void_t<PTs...>>

which always evaluates to void and introduces(?) deduced context, but the error persists. Shouldn't this work?

Another way to handle this would be to change the main ValueIsPresent for nullable types to cast to bool instead, or introduce some traits for nullable types that would tell you how to check for null values that you can specialize for PointerUnion.

The existing implementation implicitly assumes that if a type is constructible from nullptr_t, then operator== exists and works the way that's expected in this particular use case. Apparently, this doesn't work for PointerUnion.

Casting to bool works for PointerUnion, but may in theory not work for other types. I'll make this change because it is simpler, but I think the best solution would be to stop making implicit assumptions about existence / behavior of operator== / operator bool() and require the clients to explicitly provide specializations if the behavior should diverge from the default.

@s-barannikov
Copy link
Contributor Author

@kuhar PTAL
If it looks acceptable I'll change the description.

template <typename T>
struct ValueIsPresent<T, std::enable_if_t<IsNullable<T>>> {
using UnwrappedType = T;
static inline bool isPresent(const T &t) { return t != T(nullptr); }
static inline bool isPresent(const T &t) { return static_cast<bool>(t); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going down this route, it would probably make sense to also change the enable_if to check std::is_convertible<T, bool> instead?

I think you could also drop the separate std::optional specialization because it also has operator bool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going down this route, it would probably make sense to also change the enable_if to check std::is_convertible<T, bool> instead?

I think it will make much more sense, will try.

I think you could also drop the separate std::optional specialization because it also has operator bool.

They are not quite the same, the specialization for std::optional derefernces the argument of unwrapValue().
This can probably be guarded by constexpr if, but personally I find the separate specialization clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears is_convertible only handles implicit conversions, i.e. it returns false for types with explicit operator bool(). I used is_constructible with swapped arguments instead.

template <typename T>
struct ValueIsPresent<T, std::enable_if_t<IsNullable<T>>> {
struct ValueIsPresent<T, std::enable_if_t<std::is_constructible_v<bool, T>>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the check that T can be constructed with nullptr? There are types that can be cast to bool but are not pointer-like.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but figured that checking whether the type is convertible to bool exactly answers the question "does an object have a value". This may not be desired for non-class types (e.g. int) but those shouldn't really be used in this context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this enable_if guards both isPresent and unwrapValue -- don't we need extra care for the latter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I'm going to approve this as-is, use your judgement here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this enable_if guards both isPresent and unwrapValue -- don't we need extra care for the latter?

Maybe, although constructibility from nullptr_t doesn't guarantee that the default implementation of unwrapValue would fit.

As I noted in the other comment, I think it would be best if we provided explicit specializations for each type like we do with simplify_type, for example. Possibly provide a generic one for T *, too. I would't commit myself to implementing this though, I've already shown my expertise in metaprogramming.

Copy link
Member

@kuhar kuhar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unittests/Support/CMakeFiles/SupportTests.dir/Casting.cpp.o fails to build in pre-commit CI.

@s-barannikov
Copy link
Contributor Author

unittests/Support/CMakeFiles/SupportTests.dir/Casting.cpp.o fails to build in pre-commit CI.

Now it cannot select a specialization for std::optional, sigh.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants