This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

3047. atomic compound assignment operators can cause undefined behavior when corresponding fetch_meow members don't

Section: 33.5.8.3 [atomics.types.int], 33.5.8.5 [atomics.types.pointer], 33.5.8.6 [atomics.types.memop] Status: New Submitter: Tim Song Opened: 2017-12-15 Last modified: 2020-09-06

Priority: 3

View all issues with New status.

Discussion:

Given atomic<int> meow{INT_MAX};, meow.fetch_add(1) has well-defined behavior because 33.5.8.3 [atomics.types.int] p7 says that

Remarks: For signed integer types, arithmetic is defined to use two's complement representation. There are no undefined results.

but meow += 1 and ++meow have undefined behavior, because these operator functions are defined (by, respectively, 33.5.8.3 [atomics.types.int] p8 and 33.5.8.6 [atomics.types.memop]) to be equivalent to return fetch_add(1) + 1;, and so the addition of 1 to the result of fetch_add — which causes an integer overflow in this case — occurs outside the protection of fetch_add magic. Additionally, the return value might differ from what fetch_add actually wrote since that addition isn't required to use two's complement. This seems like a trap for the unwary. Is it intended?

A similar issue affects the atomic<T*> partial specialization for pointers.

[2018-01; Priority set to 3 after mailing list discussion]

[2019-04-15; JF Bastien comments and provides wording]

As discussed by LWG during the San Diego 2018 meeting, Jens removed LWG 3047 from "P1236R1: Alternative Wording for P 0907R4 Signed Integers are Two's Complement".

Proposed resolution:

This wording is relative to N4810.

  1. Modify 33.5.7.3 [atomics.ref.int] as indicated:

    integral operator op=(integral operand) const noexcept;
    

    -7- Effects: Equivalent to: return static_cast<integral>(static_cast<make_unsigned_t<integral>>(fetch_key(operand)) op static_cast<make_unsigned_t<integral>>(operand));

  2. Modify 33.5.7.6 [atomics.ref.memop] as indicated:

    T* operator++() const noexcept;
    

    -3- Effects: Equivalent to: return static_cast<T>(static_cast<make_unsigned_t<T>>(fetch_add(1)) + static_cast<make_unsigned_t<T>>(1));

    T* operator--(int) const noexcept;
    

    -4- Effects: Equivalent to: return static_cast<T>(static_cast<make_unsigned_t<T>>(fetch_sub(1)) - static_cast<make_unsigned_t<T>>(1));

  3. Modify 33.5.8.3 [atomics.types.int] as indicated:

    T operator op=(T operand) volatile noexcept;
    T operator op=(T operand) noexcept;
    

    -8- Effects: Equivalent to: return static_cast<T>(static_cast<make_unsigned_t<T>>(fetch_key(operand)) op static_cast<make_unsigned_t<T>>(operand));

    [Drafting note: atomic<integral>'s working for operator++/operator-- is shared with atomic<T*>. — end drafting note]

    [Drafting note: atomic<floating-point> seems to be correct, LWG should confirm that it is. — end drafting note]

  4. Modify 33.5.8.5 [atomics.types.pointer] as indicated:

    T* operator op=(ptrdiff_t operand) volatile noexcept;
    T* operator op=(ptrdiff_t operand) noexcept;
    

    -8- Effects: Equivalent to: return reinterpret_cast<T*>(reinterpret_cast<ptrdiff_t>(fetch_key(operand)) op operand);

    Remarks: The result may be an undefined address, but the operations otherwise have no undefined behavior.

  5. Modify 33.5.8.6 [atomics.types.memop] as indicated:

    T operator++() volatile noexcept;
    T operator++() noexcept;
    

    -3- Effects: Equivalent to: return static_cast<T>(static_cast<make_unsigned_t<T>>(fetch_add(1)) + static_cast<make_unsigned_t<T>>(1));

    T operator--() volatile noexcept;
    T operator--() noexcept;
    

    -4- Effects: Equivalent to: return static_cast<T>(static_cast<make_unsigned_t<T>>(fetch_sub(1)) - static_cast<make_unsigned_t<T>>(1));

    [Drafting note: Alternatively, LWG may want to separate the integral overload of operator++/operator-- from that of atomic<T*>. end drafting note]