Author Topic: Partially-formed Default Constructible Types  (Read 4284 times)

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Partially-formed Default Constructible Types
« on: February 25, 2019, 08:54:37 AM »
While looking up info on Regularity, I came across this article that has an interesting take on default constructibility:
Stepanov-Regularity and Partially-Formed Objects vs. C++ Value Types

The article shows an example of two rather different value type classes that both support default initialization. One is a Rect with simple int fields, the other is a Pen class implemented with the pimpl idiom which contains a private pointer.

The article defines an object as being in a partially-formed state if it can be assigned to or destroyed. Other behaviours may be undefined. An implication of that is that some fields may be uninitialized (Ex: a struct's coordinates), but not ones that need to be in a valid state during assignment or destruction (Ex: a pointer to a resource which is freed by the destructor, or during re-assignment where the old value is destructed).

Note that in the above, a pointer field doesn't necessarily have to point to a valid object, it just needs to be safe to use during destruction, or re-assignment. An example is initializing a pointer field to nullptr, rather than leaving the pointer uninitialized, dangling, or pointing to a default object. It is safe to pass nullptr to delete. This means that partially-formed objects can do minimal field initialization, and adhere to the C++ principle of not paying for things you're not using.

An interesting side note from the article, is that value type structs should have a noexcept constructor. This is in contrast to an RAII type object, used to manage an external resource, which will raise an exception from the constructor if the resource can't be allocated. Note that neither the Rect, nor the Pen class discussed in the article are RAII type objects. Rather, both objects in this article support default initialization (a constructor with no parameters). Default initialization would be unusual for RAII type objects, since their constructors usually takes parameters to identify the resource they are acquiring.

Offline Vagabond

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 1014
Re: Partially-formed Default Constructible Types
« Reply #1 on: February 25, 2019, 08:09:36 PM »
I didn't know you could do this:

Code: [Select]
int x = {};  // x == 0
Rect r = {}; // r == {0, 0, 0, 0}

Interesting read.

I understand the desire to maintain efficiency by leaving things uninitialized. I find it is definitely easier to debug if values are set to reasonable defaults, such as 0 for int. When I look at the contents of a value type and everything is default I know it was created but not fed data or all the data was default. If it contains random values I have to reason if the random data means something or if it is truly random.

Also when debugging, I prefer to get consistent results from uninitialized structs/classes by having default values. If the values are all random when uninitialized, different parts of the code base may flop between working and failing on each pass which I find harder to reason with than consistent results. The counterpoint is you have a bit better chance to become aware of bugs because an initialized default value may not cause noticeable problems upfront whereas a random value will sometimes cause a problem.

Since C++ defaults to not setting values to default, I think it makes the most sense to not artificially force this by creating constructors everywhere that do so. I do think this is one reason Java or C# code is easier to debug.

Guess that is all preference though.

-Brett

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Re: Partially-formed Default Constructible Types
« Reply #2 on: February 26, 2019, 03:02:58 AM »
Yes, not everyone is a fan of uninitialized data. It can be less reproducible, and so can create confusing bugs. Though as you've stated, the random data can also reveal bugs, that a default initialized value would have hidden. On the plus side, the compiler is often able to warn about use of uninitialized data. It depends a bit on the complexity of the code flow though. Not all things can be warned about, though most of the common cases will be.

In the end, it's more about efficiency, and not doing work that's just going to be overwritten later by a late initialization. It might be the proper value is not known until other work is done first.



As for the {} initialization syntax, it will zero initialize values in many cases.

In particular, the empty {} can be used directly after a type in value initialization. Value initialization states a number of rules for zero-initializing. One curious rule, is that it may first initialize to zero, and then do default initialization. In the Notes section of default initialization, you'll find that some object may be default initialized to indeterminate values. That actually means they won't be initialized. An example is an int local variable (but not a global, static, or thread-local variable, which is zero initialized). Hence the clause about zero-initializing first in value initialization is really about zero-initializing values that would otherwise be uninitialized. So basically, the value initialization syntax will set integers to 0 and floating point values to 0.0.

In terms of the example above (note the equal sign), we actually have aggregate initialization. Aggregate initialization has a rule where if the initializer list is shorter than the number of members to initialize, it will use value initialization on the remaining members. Hence you also get the zero-initialization here.

Example:
Code: [Select]
Rect r = {1}; // r == {1, 0, 0, 0}

Offline leeor_net

  • Administrator
  • Hero Member
  • *****
  • Posts: 2350
  • OPHD Lead Developer
    • LairWorks Entertainment
Re: Partially-formed Default Constructible Types
« Reply #3 on: February 27, 2019, 01:55:20 PM »
Don't have a lot of time for a proper response so I'll just put this out there.

Was reading the article and came across this line:

Quote
If you feel uncomfortable with this implementation, you’re letting your inner Java programmer get the better of you. Don’t. This is C++. We embrace the undefined.

No! No no no no no! This is exactly how horrifying bugs are allowed to exist. This flies in the face of defensive programming. To take the contrive example, I would write it as such:

Code: [Select]
class Rectangle
{
    int x1 = 0, y1 = 0, x2 = 0, y2 = 0;

public:
    Rectangle() = default;
};

Yes, I know that there are cost overheads to initializing values but we're not dealing with embedded systems most of the time that require this level of optimization, we're dealing with real-world programming and, perhaps more importantly, humans that are very much flawed and imperfect.

The argument could be raised "Well if you're using unititialized values in C or C++ you're not a competent programmer". I scoff at this notion as both elitist and ignoring one of the primary sources of glaring bugs in programs: simple mistakes that all developers make.

So no. Don't embrace terrible ideas that only made sense in the days of 1KB of memory. Use defensive programming techniques and that includes always ensuring that any value is in a valid state unless you have absolutely concrete reason to do otherwise.

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Re: Partially-formed Default Constructible Types
« Reply #4 on: February 28, 2019, 02:26:39 AM »
Leeor doesn't embrace the undefined ;)

Slightly out of context, I guess this relates to the author's other statement:
Quote
the simple chain of reasoning described so far has less friends than you might think. And this is why I wrote this article.

You will probably meet a lot of resistance when trying to implement your default and move constructors this way.

For a counter point, consider the following code:
Code: [Select]
Rect lotsOfThem[50000000];
LoadRectDataFromFile(lotsOfThem);

Do you really want to default initialize them first? Will an optimizer be able to help you here? I think this is the real case those rules were designed for.

Quote
So no. Don't embrace terrible ideas that only made sense in the days of 1KB of memory. Use defensive programming techniques and that includes always ensuring that any value is in a valid state unless you have absolutely concrete reason to do otherwise.

I think this is a big part of why there's been such a proliferation of computer languages. You're right, we don't live in such a constrained world anymore, people more or less have realized that, and default initialization and defensive programming techniques can help prevent bugs and wasted programmer time. But then, considering the design of both core C++, and it's standard library, it might not be the best choice of language for people who value those things. As a corollary to that, C++ might not be the best choice of language for most new programming projects these days. It does have a bit of a niche with embedded software, device drivers, and low level systems code. Maybe throw in a few small, well contained, performance critical libraries. Outside of that, I don't see it used for much other than maintenance of legacy programs written before there was such wide choice. I wouldn't recommend it to a paying client for a new greenfield project that doesn't fit its niche.

... but luckily this is hobby programming here ;)

Offline leeor_net

  • Administrator
  • Hero Member
  • *****
  • Posts: 2350
  • OPHD Lead Developer
    • LairWorks Entertainment
Re: Partially-formed Default Constructible Types
« Reply #5 on: February 28, 2019, 06:18:22 PM »
Code: [Select]
Rect lotsOfThem[50000000];
LoadRectDataFromFile(lotsOfThem);

Do you really want to default initialize them first? Will an optimizer be able to help you here? I think this is the real case those rules were designed for.


I would consider this an edge case where different programming techniques come into play. To directly answer the question, no -- in this case I absolutely would not want to initialize everything here until I actually needed them. I can see this useful in cases of say, a particle system where I want to allocate memory for a large number of particles but I won't know ahead of time how many I will need so I'd init and manage them as needed. At least, that's the example that comes to mind.

But also to be fair, this is somewhat of a contrived example... though it does illustrate your point.

In any case, not your typical use case. :)

As a corollary to that, C++ might not be the best choice of language for most new programming projects these days. ... snip ... I wouldn't recommend it to a paying client for a new greenfield project that doesn't fit its niche.

Completely agreed. As much as I enjoy C++ I also hate some of the chore-like tasks that need to be done with it... and it's ability to get developers, even experienced developers, into all sorts of trouble.
« Last Edit: February 28, 2019, 06:21:23 PM by leeor_net »

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Re: Partially-formed Default Constructible Types
« Reply #6 on: March 02, 2019, 01:50:13 PM »
I find that case surprisingly common. But then I've often worked with array data in engineering related situations. I guess it depends on what you're doing.

At any rate, C++ offers no ability to turn off default initialization for any given instance of a class. It's either all of them or none of them. So if a class default initializes, it's not going to be so great for certain array uses. Effectively, this performance concern needs to be decided up front when the class is designed, rather than when programmers choose how they're going to use the class.


Can't wait for some of those C++ upgrades that should help lift the burden on the programmer. Having a proper module system will be huge. Maybe that will even help spur having a proper package and dependency management system for C++.