Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 467
selbie Avatar asked Nov 29 '19 05:11

selbie


People also ask

Does default constructor initialize members?

Default constructors are one of the special member functions. If no constructors are declared in a class, the compiler provides an implicit inline default constructor. If you rely on an implicit default constructor, be sure to initialize members in the class definition, as shown in the previous example.

Is the default constructor always called in C++?

Implicitly-declared default constructor If no user-declared constructors of any kind are provided for a class type (struct, class, or union), the compiler will always declare a default constructor as an inline public member of its class.

Will default constructor be always called?

A constructor is always called when constructing a new object, and a constructor of every superclass of the class is also called. If no constructor is explicitly called, the default constructor is called (which may or may not be declared).

What is default constructor value in C++?

A default constructor is a constructor that either has no parameters, or if it has parameters, all the parameters have default values. If no user-defined constructor exists for a class A and one is needed, the compiler implicitly declares a default parameterless constructor A::A() .


2 Answers

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.

like image 123
Nicol Bolas Avatar answered Oct 11 '22 14:10

Nicol Bolas


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]

like image 22
curiousguy Avatar answered Oct 11 '22 14:10

curiousguy