lmi
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [lmi] default-member-initializers vs. is_trivial


From: Vadim Zeitlin
Subject: Re: [lmi] default-member-initializers vs. is_trivial
Date: Mon, 19 Apr 2021 23:30:05 +0200

On Mon, 19 Apr 2021 20:57:23 +0000 Greg Chicares <gchicares@sbcglobal.net> 
wrote:

GC> On 4/18/21 11:09 PM, Vadim Zeitlin wrote:
GC> > On Sun, 18 Apr 2021 22:50:29 +0000 Greg Chicares 
<gchicares@sbcglobal.net> wrote:
GC> > 
GC> > GC> Vadim--Do you happen to know why default member initializers
GC> > GC> prevent a default ctor from being trivial?
GC> > 
GC> >  Sorry if I'm missing something here, but for me "trivial" informally 
means
GC> > "doing nothing" and, I believe, this is exactly what the C++ Standard 
says in
GC> > a more formal way. I.e. a class with a trivial default ctor is like a POD 
C
GC> > struct -- its ctor just does nothing at all.
GC> 
GC> Even if it constructs an instance by doing "nothing", it must at least
GC> associate a name with a block of memory.

 This association is not present at the run-time, it is only implicitly
reflected in the generated code. I.e. it doesn't correspond to actually
doing anything. The task of the constructor is to actually initialize (or
not) the new object data and trivial constructor doesn't do it.

GC> Either that memory is initialized in some way (e.g., all zeros), or not
GC> at all.

 It's not initialized at all.

GC> As an experiment, I tried to contrive a trivially-default-constructible
GC> struct that can't validly be initialized with all zeros:
GC> 
GC> #include <type_traits>
GC> 
GC> enum E {e0 = 3, e1 = 17}; // none of the enumerated values is zero
GC> struct S {E e;};
GC> 
GC> static_assert(std::is_trivially_constructible_v<S>);
GC> static_assert(std::is_trivially_default_constructible_v<S>);
GC> static_assert(std::is_trivially_copy_constructible_v<S>);
GC> static_assert(std::is_trivially_move_constructible_v<S>);

 This is an interesting case, I wouldn't have thought of trying this. And I
wouldn't have been 100% sure that is_trivially_default_constructible_v is
true in this case, to be honest.

 BTW, you don't need S at all here, the same thing is true for E itself.

GC> int main(int, char*[])
GC> {
GC>     S s;
GC>     std::cout << s.e << std::endl;
GC>     return 0;
GC> }
GC> 
GC> The static assertions succeed, but the stream-output line fails to compile:
GC>   error: ‘s.S::e’ is used uninitialized

 It's not an error, but a warning. And if you run the program it can output
0 (as it does here) or anything else. The initial contents is not defined
and accessing it is, of course, undefined behaviour.

GC> Then I tried
GC> - struct S {E e;};
GC> + struct S {float e;}; // 'float' naturally defaults to 0.f
GC> 
GC> but compiling it with that modification again gives:
GC>   ‘s.S::e’ is used uninitialized
GC> 
GC> It builds and runs with this modification:
GC> -    std::cout << s.e << std::endl;
GC> +    std::cout << sizeof(s.e) << std::endl;

 Well, this one is trivial, it's a compile-time construct, so what's the
interest of doing this?

GC> Is this exactly what's expected?

 I'd definitely expect s.e to be uninitialized, yes. That it is still
deemed to be trivially default constructible is a bit more surprising, but
it corresponds to the fact that it has a trivial default ctor. The fact
that this ctor doesn't leave the object in an immediately usable state
doesn't change anything.

GC> I suppose that's useful for...creating a vector of objects that cannot
GC> be used because they're uninitialized? Was that the goal? Why would
GC> such a goal be desirable? Surely I'm still missing something.

 Sorry, but this seems a very strange question to me. What's the goal of
having C structs that always behave like this? It's exactly the same thing
in C++: at this level, you define a struct to be able to name chunks of
memory and operate with them in type-safe way. There are plenty of reasons
it can be good even if you don't initialize this memory. The most common
ones I can think of would be:

0. You don't care about the initial memory contents because you're going
   to overwrite it anyhow, e.g. by copying (whether trivially or not)
   another object.

1. You can initialize the memory later more efficiently, e.g. by calling
   OS-level function zeroing the entire memory page (which is typically
   a constant-time operation and is provided by most OS, AFAIK).

2. As a special case of the previous point, you might know that the memory
   is already initialized and, in particular, zeroed, e.g. when a page was
   just mapped into the process address space.

3. As a special case of the previous point, you might know that the memory
   is already initialized with some non-trivial data, e.g. when using a
   memory-mapped file. Or, slightly more exotically, if this memory is
   mapped to I/O ports of some device (this used to be very common, but
   I have no idea if kernel drivers still work like this nowadays). Note
   that in both cases actually initializing the memory, even by zeroing it,
   would be not only useless, as in the cases above, but catastrophic.

I'm sure there are other scenarios, but, mostly, this is just how things
have always worked for our forbears, and so, of course, this must be the
only way for them to work. Wait, maybe this argument isn't that persuasive,
after all... But, still, why are you suddenly surprised by this when it
has, indeed, always worked like this since the very beginnings of C?

GC> Okay, my reasoning was that
GC>   trivially copyable <--> implementable as memcpy()
GC> so the underlying concept appeared to be
GC>   trivially anything <--> translates to something like REPNE MOVSB
GC> or IOW
GC>   trivially XXX <--> implementable as memXXX()
GC> from which it might follow that
GC>   trivially constructible <-?-> implementable as memset()
GC> but the Standard doesn't even allow memset() here.

 You wouldn't be able to use memset() to initialize structs using member
initializers. But trivially constructible doesn't use memset() at all
anyhow, it just doesn't do anything. Using memset(0) would be logical too,
but this wouldn't change much for your question and this is definitely not
the case today, although I believe there were some proposals to change
this.

 Regards,
VZ

Attachment: pgpygGe0O13H5.pgp
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]