Skip to content

CWG3232 [expr.add] Undefined behavior in pointer subtraction has been removed accidentally #922

Description

@eisenwave

Subclause: [expr.add]

Issue description

P3899R3 accidentally removed undefined behavior in the case where the result of subtracting pointers is not representable as std::ptrdiff_t. This was caused by limiting undefined behavior in subtraction to arithmetic types, which pointers are not.

Proposed resolution

Make the note in [expr.add] paragraph 5 normative text:

 The result of subtracting two pointer expressions P and Q
 is a prvalue of type std​::​ptrdiff_t ([support.types.layout]).
     (5.1) If P and Q both evaluate to null pointer values,
     the value is 0.
     (5.2) Otherwise, if P and Q point to, respectively,
     array elements i and j of the same array object x,
     the expression P - Q has the value i−j.
-    [Note 2:
     If the value i−j is not in the range of representable values
     of type std​::​ptrdiff_t, the behavior is undefined
-    ([expr.pre])
     .
-    — end note]
     (5.3) Otherwise, the behavior is undefined.

Rename [ub:expr.add.sub.diff.pointers] to [ub:expr.add.sub.pointers.array], and add a corresponding UB annex entry [ub:expr.add.sub.pointers.range]:

Behavior is undefined when two expressions of pointer type are subtracted and the difference is not representable in std::ptrdiff_t.
[Example:

char big[static_cast<std::uintmax_t>(PTRDIFF_MAX) + 1];
std::ptrdiff_t d = std::end(big) - std::begin(big); // undefined behavior

— end example]

Activity

  1. notadragon commented on Jul 28, 2026

    @notadragon

    I would call this one expr.sub.pointers.representable. Avoid range, because "range" when talking about pointers is a very overloaded term.

    The actual change here seems fine, but it is on the border of editorial --- it's correcting a normative mistake in a paper that was merged, but it is definitely a normative and not completely editorial change.

    Unless i'm missing something obvious, the example doesn't seem to have UB --- the value of that difference is PTRDIFF_MAX, which seems like a value that should be representable as a std::ptrdiff_t. I haven't been able to come up with a meaningful example that would work on anything like a real platform and exhibit this particular UB.

  2. eisenwave commented on Jul 28, 2026

    @eisenwave
    MemberAuthor

    The actual change here seems fine, but it is on the border of editorial --- it's correcting a normative mistake in a paper that was merged, but it is definitely a normative and not completely editorial change.

    It is borderline editorial, but I think this would be one of those editorial PRs that CWG should look at anyway, so we don't lose much by going through issue handling. It's also valuable to get some feedback on whether we want to cover this using some [expr.pre] blanket wording again (though that has always been surprising, which is why we have this note in the first place), or whether we want to make the note normative.

    Unless I'm missing something obvious, the example doesn't seem to have UB --- the value of that difference is PTRDIFF_MAX, which seems like a value that should be representable as a std::ptrdiff_t.

    Hmm yeah, it needs to be PTRDIFF_MAX + 1 for the example to work. Fixed.

  3. notadragon commented on Jul 28, 2026

    @notadragon

    The unfortunate part is there's no way to get there in a real example --- no common platforms yet you get an object that big in the first place.

  4. eisenwave commented on Jul 28, 2026

    @eisenwave
    MemberAuthor

    You're not going to get this behavior on a 64-bit platform, but maybe on a 16-bit platform or 32-bit platform, you might be allowed to malloc more than half your address space. Or maybe you could get there with freestanding, or memory mapping, or anything like that.

  5. safocl commented on Jul 28, 2026

    @safocl

    I don't think we should make such edits, since this reference in the form of a note already refers to the relevant provisions of the standard ([expr.pre#4.5]) - which is quite sufficient, since the type of operation is already clearly indicated here.

    [expr.add#5]

    5 The result of subtracting two pointer expressions P and Q is a prvalue of type std​::​ptrdiff_t ([support.types.layout]).

    [expr.pre#4.5]

    The behavior of evaluating an arithmetic expression is undefined ([ub:expr.expr.eval]) if the mathematical result is neither

    4.5 in the range of representable values for its type nor
    4.6 a negative infinity, positive infinity, or NaN that is among the values of the type[.](https://eel.is/c++draft/expr.pre#4.sentence-3)
    
  6. notadragon commented on Jul 28, 2026

    @notadragon

    Pointers are not of arithmetic type, so the difference of two pointers is not an arithmetic expression. That''s the hole that was opened up by P3899R3.

  7. safocl commented on Jul 28, 2026

    @safocl

    Pointers are not of arithmetic type, so the difference of two pointers is not an arithmetic expression. That''s the hole that was opened up by P3899R3.

    Yes, perhaps this is exactly what should be changed?
    (Above, I wanted to suggest that the existing rule could be reused for pointers as well — it seems like it wouldn't introduce any negative aspects.)

  8. safocl commented on Jul 28, 2026

    @safocl

    Perhaps it's worth adding operations on pointers to the "arithmetic operations"?

    4 An [arithmetic expression](https://eel.is/c++draft/expr.pre#def:expression,arithmetic) is
      4.1 a unary plus or minus ([[expr.unary.op]](https://eel.is/c++draft/expr.unary.op)),
      4.2 an addition ([[expr.add]](https://eel.is/c++draft/expr.add)),
      4.3 a subtraction ([[expr.sub]](https://eel.is/c++draft/expr.sub)), or
      4.4 a multiplication, division, or remainder ([[expr.mul]](https://eel.is/c++draft/expr.mul))
    expression where every (possibly converted) operand is of arithmetic 
    + or pointer
    type[.](https://eel.is/c++draft/expr.pre#4.sentence-1)

    although this will lead to some absurd actions for pointers (like multiplication)

  9. notadragon commented on Jul 28, 2026

    @notadragon

    P3899 makes some points about this, and i think it's right. There's no such thing as the "mathematical result" of any operation with pointers --- pointers aren't numbers. We define elsewhere a number of operations that can be done with pointers, but none of those things are arithmetic operations on the pointer --- they are defined by the language, and the abstract machine, not by math. Mixing the stuff in expr.pre t include operations that are not defined by math just seems to be a category error (that was fixed by P3899, that adding pointer back to that list would just reintroduce).

  10. safocl commented on Jul 28, 2026

    @safocl

    I think the example correction in the original post is simple to implement, but perhaps it's worth considering a more general change that would produce the same results without affecting other situations?

    That said, I would still replace "If the value i−j" in the original post with "if the result" (similar to [expr.pre#4]).

     The result of subtracting two pointer expressions P and Q
     is a prvalue of type std​::​ptrdiff_t ([support.types.layout]).
         (5.1) If P and Q both evaluate to null pointer values,
         the value is 0.
         (5.2) Otherwise, if P and Q point to, respectively,
         array elements i and j of the same array object x,
         the expression P - Q has the value i−j.
    -    [Note 2:
         If the 
    -    value i−j
    +    result
         is not in the range of representable values
         of type std​::​ptrdiff_t, the behavior is undefined
    -    ([expr.pre])
         .
    -    — end note]
         (5.3) Otherwise, the behavior is undefined.
  11. notadragon commented on Jul 29, 2026

    @notadragon

    That would seem fine to me. I will leave it up to @eisenwave and @jensmaurer how we want to deal with this one though (land as part of P4284 with the annex entry, land as a separate CWG issue and do the annex entry later, land as an editorial issue and keep the annex entry in P4284 to land after the editorial issue, etc.).

  12. eisenwave commented on Jul 29, 2026

    @eisenwave
    MemberAuthor

    I think we should always put any UB annex changes into the CWG issues that affect UB, so CWG can easily review these as a holistic package. The "we'll do the annex entry later" approach is just asking for the UB annex to get out of sync. "We'll do it later" has a way of happening years later or never in WG21.

    CWG hasn't yet decided how to deal with this pointer subtraction UB wording, so it's also unclear what the annex entry looks like, and whatever you put into P4284 might be subject to major changes through this CWG issue.

  13. changed the title [-][expr.add] Undefined behavior in pointer subtraction has been removed accidentally[/-] [+]CWG3232 [expr.add] Undefined behavior in pointer subtraction has been removed accidentally[/+] on Sep 20, 2026
  14. jensmaurer commented on Sep 20, 2026

    @jensmaurer
    Member
  15. eisenwave commented on Sep 20, 2026

    @eisenwave
    MemberAuthor

    CWG3232

    @jensmaurer it looks like you flipped std::end and std::begin in the UB annex proposed wording ... but I guess we do allow that? Nvm, the result doesn't need to be positive, but then isn't the example off by one because ptrdiff_t can represent one more negative value than positive value?

  16. jensmaurer commented on Sep 20, 2026

    @jensmaurer
    Member

    Yup, fixed.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions