6

After reading about the standard C11 version of Martin Uecker's ICE_P predicate, I tried to implement it in pure C++. The C11 version, making use of _Generic selection is as follows:

#define ICE_P(x) _Generic((1? (void *) ((x)*0) : (int *) 0), int*: 1, void*: 0)

The obvious approach for C++ is to replace _Generic by a template and decltype, such as:

template<typename T> struct is_ice_helper;
template<> struct is_ice_helper<void*> { enum { value = false }; };
template<> struct is_ice_helper<int*>  { enum { value = true  }; };

#define ICE_P(x) (is_ice_helper<decltype(1? (void *) ((x)*0) : (int *) 0)>::value)

However, it fails the simplest test. Why can't it detect integer constant expressions?

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458

1 Answers1

4

The issue is subtle. The specification for determining the composite type of the conditional expression's pointer operands are similar in C++ to the ones in C, so it starts off looking promising:

(N4659) [expr.cond]

7 Lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold:

  • [...]

  • One or both of the second and third operands have pointer type; pointer conversions, function pointer conversions, and qualification conversions are performed to bring them to their composite pointer type (Clause [expr]). The result is of the composite pointer type.

  • [...]

The reduction to the composite pointer type is specified as follows:

(N4659) [expr]

5 The composite pointer type of two operands p1 and p2 having types T1 and T2, respectively, where at least one is a pointer or pointer to member type or std​::​nullptr_­t, is:

  • if both p1 and p2 are null pointer constants, std​::​nullptr_­t;
  • if either p1 or p2 is a null pointer constant, T2 or T1, respectively;
  • if T1 or T2 is “pointer to cv1 void” and the other type is “pointer to cv2 T”, where T is an object type or void, “pointer to cv12 void”, where cv12 is the union of cv1 and cv2;
  • [...]

So the result of our ICE_P macro is determined by which of the bullets above we land one after checking each in order. Given how we defined is_ice_helper, we know that the composite type is not nullptr_t, otherwise we'd hit the first bullet, and will get an error due to the missing template specialization. So we must be hitting bullet number 3, making the predicate report false. It all seems to hinge on the definition of a null pointer constant.

(N4659) [conv.ptr] (emphasis mine)

1 A null pointer constant is an integer literal with value zero or a prvalue of type std​::​nullptr_­t. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type. Such a conversion is called a null pointer conversion. Two null pointer values of the same type shall compare equal. The conversion of a null pointer constant to a pointer to cv-qualified type is a single conversion, and not the sequence of a pointer conversion followed by a qualification conversion. A null pointer constant of integral type can be converted to a prvalue of type std​::​nullptr_­t.

Since (int*)0 is not a null pointer constant by the definition above, we do not qualify for the first bullet of [expr]/5. The composite type is not std::nullptr_t. Neither is (void *) ((x)*0) a null pointer constant, nor can it be turned into one. Removing the cast (something the definition doesn't allow) leaves us with (x)*0. This is a integer constant expression with value zero. But it is not an integer literal with value zero! The definition of a null pointer constant in C++ deviates from the one in C!

(N1570) 6.3.2.3 Pointers

3 An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

C allows arbitrary constant expressions with value zero to form a null pointer constant, while C++ requires integer literals. Given C++'s rich support for computing constant expressions of a variety of literal types, this seems like a needless restriction. And one that makes the above approach to ICE_P a non-starter in C++.

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • I think the restriction can about because `1 - 1` shouldn't be a null pointer constant. Don't know why they choose to disallow `(int*)0` being a null pointer constant though. – Rakete1111 Apr 21 '19 at 12:12
  • 1
    @Rakete1111 - Regarding `1 - 1`. The wording was changed between 2003 and 2011 (see [n1905](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1905.pdf) §4.10). So it does seem intentional, and I'd love to know the motivation (couldn't find it). As for `(int*)` that kinda makes sense. The only useful null pointer constant is the sort that can be converted to any pointer type, and `(int*)0` doesn't qualify. – StoryTeller - Unslander Monica Apr 21 '19 at 12:17
  • @Rakete1111 - I'd say I can't find it a moment before I'm able to... [CWG 903](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#903) is the reason. But I can't help but feel that the resolution kinda threw the baby out with the bath water. – StoryTeller - Unslander Monica Apr 21 '19 at 12:21
  • In `(int*)0`, is `0` the null pointer constant which is converted by the cast to a null pointer to int value (but no constant)? That seems ... bath water. – Peter - Reinstate Monica Aug 26 '20 at 22:00
  • 1
    @Peter - Yup. 0 is a null pointer constant, converted to a null pointer value of `int*`. But `(int*)0` as a whole not a "null pointer constant", despite being a constant expression. It's almost amusing. – StoryTeller - Unslander Monica Aug 26 '20 at 22:07