Why does default constructor of std::atomic not default initialize the underlying stored value?
Asked Answered
A

3

12

Since it's Thanksgiving today in the USA, I'll be the designated turkey to ask this question:

Take something as innocuous as this. An atomic with a simple plain old data type such as an int:

atomic<int> x;
cout << x;

The above will print out garbage (undefined) data. Which makes sense given what I read for the atomic constuctor:

(1) default constructor

Leaves the atomic object in an uninitialized state. An uninitialized atomic object may later be initialized by calling atomic_init.

Feels like an odd committee decision. But I'm sure they had their reasons. But I can't think of another std:: class where the default constructor will leave the object in an undefined state.

I can see how it would make sense for more complex types being used with std::atomic that don't have a default constructor and need to go the atomic_init path. But the more general case is to use an atomic with a simple type for scenarios such as reference counting, sequential identifier values, and simple poll based locking. As such it feels weird for these types to be not have their own stored value "zero-initialized" (default initialized). Or at the very least, why have a default constructor if isn't going to be predictable.

What's the rationale for this where an uninitialized std::atomic instance would be useful.

Amaral answered 29/11, 2019 at 5:54 Comment(4)
No, what's really weird is that doing atomic<int> i{}; doesn't zero-initialize them either.Weld
The default ctor needs to establish the internal invariants of a type. But value initialization should establish a valid state at least for copy so that T x = T(); works. But copying atomics doesn't really make sense as atomics are not about values about about behavior of basic operations (and a copy isn't an atomic operation).Leucine
std::array<int, 5> a; also leaves a[0] uninitialized.Basso
But I can't think of another std:: class where the default constructor will leave the object in an undefined state. - std::size_t or std::uint32_t are examples of that. They're typedefs not classes, but primitive types are a common use-case for the T in std::atomic<T>.Laager
W
11

As mentioned in P0883, the main reason for this behavior is compatibility with C. Obviously C has no notion of value initialization; atomic_int i; performs no initialization. To be compatible with C, the C++ equivalent must also perform no initialization. And since atomic_int in C++ is supposed to be an alias for std::atomic<int>, then for full C/C++ compatibility, that type too must perform no initialization.

Fortunately, C++20 looks to be undoing this behavior.

Weld answered 29/11, 2019 at 6:34 Comment(5)
Wow. That just seems insane to future proof compatibility with C by having the C++ version weakened and bugged. I see Herb Sutter has addressed this. And that is good enough for me.Amaral
I'm not sure that argument fully holds up. The object-representation and mapping of atomic ordering to asm instructions can be compatible with C without this, for passing an atomic_int* between C and C++. No-init would only matter for being source compatible. And reading an uninitialized value is UB (I think even for atomic?) so there must be an atomic store before the first read anyway. Also only relevant for automatic storage: static storage is zero-initialized, so it can't apply to a global extern "C" std::atomic_int fooLaager
But yes, since you linked P0883, it's not your answer I'm disagreeing with, it's the reasoning cited by that document for why C++ works this way.Laager
@Amaral Weakened, maybe. Bugged? That's a pretty strong word for following the same semantics as plain scalar types.Leucine
C++20 will default initialize it. Refer to https://mcmap.net/q/930585/-why-does-default-constructor-of-std-atomic-not-default-initialize-the-underlying-stored-valueAzaleeazan
A
3

In C++ 20, std::atomic will default initialize the value with limitations:

  1. The type must be default constructible
  2. The initialization is not atomic.

Quote from cppreference

  1. The default constructor is trivial: no initialization takes place other than zero initialization of static and thread-local objects. std::atomic_init may be used to complete initialization. (until C++20)
  2. Value-initializes the underlying object (i.e. with T()). The initialization is not atomic. Use of the default constructor is ill-formed if std::is_default_constructible_v<T> is false. (since C++20)

Live Demo compares C++20 with C++17

#include <iostream>
#include <atomic>
#include <array>
int main()
{
    std::cout << "atomic";
    std::array<std::atomic<int>, 5> ar;
    for(const auto& item: ar){
        std::cout << " " << item;
    }
    std::cout << "\nstatic atomic";
    static std::array<std::atomic<int>, 5> sar;
    for(const auto& item: sar){
        std::cout<< " " << item;
    }
} 

Result of C++ 20

atomic 0 0 0 0 0
static atomic 0 0 0 0 0

Result of C++17 (non initialized values are random garbage value)

atomic -951377176 32655 4198992 0 0
static atomic 0 0 0 0 0
Azaleeazan answered 17/8, 2023 at 10:19 Comment(2)
Did you forget to put static in the declaration of sar? If not, what do you mean by "static atomic"? If you had used static storage, the values would all be zero even with C++17. (Your live demo has different code, with only one array, and no comments mentioning variations, just compiling with GCC -std=c++20 or -std=c++14. (Why not C++17 since that's the last version that didn't require zero-init?))Laager
Thank you @PeterCordes. I update the live demo, code and output accordingly. static atomic integer should be initialized to 0 in C++17.Azaleeazan
L
2

What's the rationale for this where an uninitialized std::atomic instance would be useful.

For the same reason basic "building block" user defined types should not do more than strictly needed, especially in unavoidable operations like construction.

But I can't think of another std:: class where the default constructor will leave the object in an undefined state.

That's the case of all classes that don't need an internal invariant.

There is no expectation in generic code that T x; will create a zero initialized object; but it's expected that it will create an object in a usable state. For a scalar type, any existing object is usable during its lifetime.

On the other hand, it's expected that

T x = T();

will create an object in a default state for generic code, for a normal value type. (It will normally be a "zero value" if the values being represented have such thing.)

Atomics are very different, they exist in a different "world"

Atomics aren't really about a range of values. They are about providing special guarantees for both reads, writes, and complex operations; atomics are unlike other data types in a lot of ways, as no compound assignment operation is ever defined in term of a normal assignment over that object. So usual equivalences don't hold for atomics. You can't reason on atomics as you do on normal objects.

You simply can't write generic code over atomics and normal objects; it would make no sense what so ever.

(See footnote.)

Summary

  • You can have generic code, but not atomic-non atomic generic algorithms as their semantic don't belong in the same style of semantic definition (and it isn't even clear how C++ has both atomic and non atomic actions).
  • "You don't pay for what you don't use."
  • No generic code will assume that an uninitialized variable has a value; only that it's in a valid state for assignment and other operations that don't depend on the previous value (no compound assignment obviously).
  • Many STL types are not initialized to a "zero" or default value by their default constructor.

[Footnote:

The following is "a rant" that is a technical important text, but not important to understand why the constructor of an atomic object is as it is.

They simply follow different semantic rules, in the most extremely deep way: in a way the standard doesn't even describe, as the standard never explains the most basic fact of multithreading: that some parts of the language are evaluated as a sequence of operations making progress, and that other areas (atomics, try_lock...) don't. In fact the authors of the standard clearly do not even see that distinction and do not even understand that duality. (Note that discussing these issues will often get your questions and answers both downvoted and deleted.)

This distinction is essential as without it (and again, it appears nowhere in the standard), exactly zero programs can even have multithread-defined behavior: only old style pre thread behavior can be explained without this duality.

The symptom of the C++ committee not getting what C++ is about is the fact they believe the "no thin air value" is a bonus feature and not an essential part of the semantics (not getting "no thin air" guarantee for atomics make the promise of sequential semantic for sequential programs even more indefensible).

--end note]

Leucine answered 30/11, 2019 at 0:1 Comment(11)
As I explained in an answer to your question What formally guarantees that non-atomic variables can't see out-of-thin-air values and create a data race like atomic relaxed theoretically can?, it's pretty clear that the authors of the C++ standard do not consider out-of-thin-air values to be something that an implementation can optionally allow. They just haven't figured out how to formally disallow it without a rule that's too strong in other ways. This answer starts off well but the overly dramatic rant seems misplaced and unnecessary.Laager
The argument about int itself not having a default constructor makes a lot of sense. Nicol's point (in comments) about even atomic<int> i{} not zero-initializing isn't explained by this argument, but that's not what the question asked.Laager
@PeterCordes No real implementation can have those and it wasn't meant to be considered acceptable, but the std fails to even informally say that they are strictly forbidden and impossible under the correct interpretation of the intent. But the underlying issue is that the std has too much emphasis on specification and too little on intent. If intent was clear as in the writings of Stroustrup, the readers would know how to deal with ambiguities and contradictions (as they always have to do in practice, dismissing the actual words). And the mess that is the std text is dramatic for me.Leucine
That's a good point in general. The current wording of that specific informal requirement is fairly strong, though. But this kind of thing has always been a "problem" for writing real-world software in C++, not just ivory tower code that only depends on purely ISO C++ constructs. Maybe the committee could or should do more to change that by plainly guaranteeing more stuff.Laager
I know my writings about the std or other's criticism are often qualified as "rants" because they are strong, but I only "rant" when I feel an issue needs to be dealt with before everything else in a domain (be it exception specifications or MT specification). I couldn't avoid that "ranting" matter here w/o losing the essential diff. between atomic semantic and normal sequential semantic (the later almost including the whole pthreads). Atomic (non at least fully acq_rel ops) semantic is almost as remote to sequential ops as Prolog is to Pascal.Leucine
@PeterCordes "the overly dramatic rant seems misplaced and unnecessary" The rant is important but can be dismissed if one takes for granted that atomics are entirely different entities that don't have to follow the same rules and that have semantics so different that generic code won't mix them with normal objects. Can I make a collapsible window for the rant?Leucine
AFAIK you can't get collapsible text in SO markdown. Spoiler text is hidden but still takes up the full amount of blank space. Anyway, I think it's easy to just say that atomic behave very differently to non-atomic objects (possible reordering and so on) so you shouldn't think about atomic<int> as just another kind of int. (Although that goes against your earlier argument that int x; isn't initialized.) Anyway, I don't see a need to claim that the C++ committee doesn't "get it" and your "truther" answers get downvoted.Laager
Also, the atomic<T> constructor is not itself atomic. Arguments based on the semantics of using member functions / operators of the atomic<T> class fall flat for me.Laager
Let us continue this discussion in chat.Leucine
For the record, users with 10k rep can see deleted answers, so there's a tiny bit of transparency that way. Other than that, will moving to chat.Laager
Followup to the discussion here about ISO C++ not formally disallowing out-of-thin-air values: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0668r5.html points out that they'd like to, but haven't been able to formulate the rules in a way that disallows such values without also imposing other restrictions they don't want to impose. My answer on a linked question quotes the relevant part it. IMHO, it's widely understood by implementers and users that this won't happen in practice.Laager

© 2022 - 2024 — McMap. All rights reserved.