Bug 79658 - [-Wuninitialized] referencing uninitialized field of POD struct should warn
Summary: [-Wuninitialized] referencing uninitialized field of POD struct should warn
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: unknown
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: diagnostic
Depends on:
Blocks: Wuninitialized 89976
  Show dependency treegraph
 
Reported: 2017-02-21 10:46 UTC by Pedro Alves
Modified: 2021-04-01 23:10 UTC (History)
4 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2017-08-22 00:00:00


Attachments
reproducer (628 bytes, text/plain)
2017-02-21 10:50 UTC, Pedro Alves
Details
reproducer v2 (296 bytes, text/plain)
2017-02-21 14:49 UTC, Pedro Alves
Details
reproducer v3 (274 bytes, text/plain)
2017-02-21 15:16 UTC, Pedro Alves
Details
reproducer v4 (264 bytes, text/plain)
2017-02-24 12:32 UTC, Pedro Alves
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Pedro Alves 2017-02-21 10:46:15 UTC
In this code:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
template <typename E>
struct enum_flags
{
  enum_flags &operator|= (E e)
  {
    m_enum_value = (E) (m_enum_value | e); // uses uninitialized m_enum_value
    return *this;
  }

  E m_enum_value;
};

enum flag
  {
    FLAG1 = 1
  };

void
foo ()
{
  enum_flags<flag> ef;

  ef |= FLAG1; // missing warning here
}

void
bar ()
{
  flag f;

  f = (flag) (f | FLAG1);  // warning here
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I expect to get a warning on the "missing warning here" line,
because inside operator|= there's a reference to the uninitialized
m_enum_value field.

However, compiling the code below using GCC 7.0.1 20170221 (current trunk),
with "-O2 -Wall -Wextra -Wuninitialized" does not catch that bug.

I tried adding __attribute__((__always_inline__)) to the operator|= method,
to try to help gcc see through the method, but it makes no difference.

Note there's no user-defined ctor here.

The equivalent code using raw enums directly like in the "bar" function, does warn on the "warning here" line.

Enabling optimization does NOT help.

Here's current G++ output with g++ 7.0.1 20170221 (experimental):

 $ /opt/gcc/bin/g++ enum_flags.cc -o enum_flags -g3 -O2 -c -Wall -Wextra -Wuninitialized
 enum_flags.cc: In function ‘void bar()’:
 enum_flags.cc:31:17: warning: ‘f’ is used uninitialized in this function [-Wuninitialized]
    f = (flag) (f | FLAG1);
               ~~~^~~~~~~~
Comment 1 Pedro Alves 2017-02-21 10:50:03 UTC
Created attachment 40797 [details]
reproducer

Here's the same reproducer source.
Comment 2 Richard Biener 2017-02-21 14:00:02 UTC
For your testcase foo () is quickly optimized to nothing given ef is unused.  For -O0 we only warn before inlining.
Comment 3 Pedro Alves 2017-02-21 14:06:35 UTC
> For your testcase foo () is quickly optimized to nothing given ef is unused.  

Shouldn't that be true for "bar ()" too then?

Looks like I over reduced.  I'll try again.
Comment 4 Pedro Alves 2017-02-21 14:45:54 UTC
Here's a better one:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
template <typename E>
struct enum_flags
{
  enum_flags &operator|= (E e)
  {
    m_enum_value = (E) (m_enum_value | e);
    return *this;
  }

  operator E () const
  { return m_enum_value; }

  E m_enum_value;
};

enum flag
  {
    FLAG1 = 1,
  };

typedef enum_flags<flag> eflags;

extern void bar ();

void
foo_0 (int param)
{
  eflags f0;

  if (param)
    f0 |= FLAG1;

  if (f0 != 0) // doesn't warn
    bar ();
}

void
foo_1 (int param)
{
  eflags f1;

  if (param)
    f1 |= FLAG1;

  if (f1 != FLAG1) // warns
    bar ();
}

void
foo_2 (int param)
{
  eflags f2;

  if (param)
    f2 |= FLAG1;

  if (f2 != 1) // doesn't warn
    bar ();
}

void
foo_3 (int param)
{
  eflags f3;

  if (param)
    f3 |= FLAG1;

  if (f3 != 3) // warns
    bar ();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We get:
$ /opt/gcc/bin/g++ -std=gnu++11 enum_flags2.cc -o enum_flags.o -g3 -O2 -Wuninitialized -c -Wall -Wextra

enum_flags2.cc: In function ‘void foo_1(int)’:
enum_flags2.cc:6:25: warning: ‘f1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     m_enum_value = (E) (m_enum_value | e);
                         ^~~~~~~~~~~~
enum_flags2.cc:40:10: note: ‘f1’ was declared here
   eflags f1;
          ^~
enum_flags2.cc: In function ‘void foo_3(int)’:
enum_flags2.cc:6:25: warning: ‘f3’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     m_enum_value = (E) (m_enum_value | e);
                         ^~~~~~~~~~~~
enum_flags2.cc:64:10: note: ‘f3’ was declared here
   eflags f3;
          ^~

In particular, note that the uses in foo_0 and foo_2 did NOT warn while the uses in foo_1 and foo_3 did warn.  Why's that?

The use I was originally most interested in catching was the one like in foo_0, where we compare against 0.  The original code has means to make comparing enum_flags against other integer literals fail to compile.  (0 means bit flags not set, the code is implementing a type-safe enum flags wrapper).  I just tried the other values for completeness, thinking that maybe 0 was being given "special" treatment, though foo_2 seems to show otherwise.

FWIW, adding enum values to cover all the compared-to values makes no difference:

enum flag
  {
    FLAG0 = 0,
    FLAG1 = 1,
    FLAG2 = 2,
    FLAG3 = 3,
  };

Same set of warnings.
Comment 5 Pedro Alves 2017-02-21 14:49:08 UTC
Created attachment 40806 [details]
reproducer v2

Same code as the previous comment.
Comment 6 Pedro Alves 2017-02-21 15:16:59 UTC
Created attachment 40807 [details]
reproducer v3

Slightly reduced testcase.  enum_flags no longer a template, and stores/uses/converts to int instead of enum type.  Same set of (missing) warnings.
Comment 7 Pedro Alves 2017-02-24 12:29:08 UTC
Today I remembered to try reducing the v3 reproducer by converting to C instead of C++.

The resulting warnings are not exactly the same, but they're very similar.  The fact that we still only get warnings for foo_1 and foo_3 is preserved.
All the foo_* functions are identical except for the literal being compared to.  Somehow comparing against 0, 1, 3, or FLAG1 behaves differently.  Comparing against 1 doesn't warn, but against FLAG1 (which is 1)
does.  Quite mystifying.

============================================
struct enum_flags { int value; };

static inline void set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
static inline int get_flag (struct enum_flags *f) { return f->value; }

enum flag { FLAG1 = 1, };

extern void bar ();

void
foo_0 (int param)
{
  struct enum_flags f0; // doesn't warn

  if (param)
    set_flag (&f0, FLAG1);

  if (get_flag (&f0) != 0) // doesn't warn
    bar ();
}

void
foo_1 (int param)
{
  struct enum_flags f1; // warns

  if (param)
    set_flag (&f1, FLAG1);

  if (get_flag (&f1) != FLAG1) // warns
    bar ();
}

void
foo_2 (int param)
{
  struct enum_flags f2; // doesn't warn

  if (param)
    set_flag (&f2, FLAG1);

  if (get_flag (&f2) != 1)
    bar ();
}

void
foo_3 (int param)
{
  struct enum_flags f3; // warns

  if (param)
    set_flag (&f3, FLAG1);

  if (get_flag (&f3) != 3)
    bar ();
}
============================================

Results in:

============================================
$ /opt/gcc/bin/gcc enum_flags4.c -o enum_flags -O2 -Wall -Werror -Wuninitialized -c
enum_flags4.c: In function ‘foo_1’:
enum_flags4.c:3:81: error: ‘f1.value’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
 static inline void set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
                                                                        ~~~~~~~~~^~~
enum_flags4.c:25:21: note: ‘f1.value’ was declared here
   struct enum_flags f1; // warns
                     ^~
enum_flags4.c: In function ‘foo_3’:
enum_flags4.c:3:81: error: ‘f3.value’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
 static inline void set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
                                                                        ~~~~~~~~~^~~
enum_flags4.c:49:21: note: ‘f3.value’ was declared here
   struct enum_flags f3; // warns
                     ^~
cc1: all warnings being treated as errors
============================================
Comment 8 Pedro Alves 2017-02-24 12:32:30 UTC
Created attachment 40823 [details]
reproducer v4

Same as above.
Comment 9 Eric Gallager 2017-08-22 22:49:47 UTC
confirmed that reproducer v4 warns for f1 and f3 but not f0 or f2.
Comment 10 Martin Sebor 2021-04-01 23:10:03 UTC
Many test cases here...

For the test case in comment #0, GCC 11 still diagnoses only the second function.  The first was never diagnosed because the variable is optimized out.  That should be expected.  When the variable is used it is diagnosed.

$ gcc -O2 -S -Wall pr79658-c0.C 
pr79658-c0.C: In function ‘void bar()’:
pr79658-c0.C:31:15: warning: ‘f’ is used uninitialized [-Wuninitialized]
   31 |   f = (flag) (f | FLAG1);  // warning here
      |               ^
pr79658-c0.C:29:8: note: ‘f’ was declared here
   29 |   flag f;
      |        ^

For the test case in comment #4, with -O, GCC 11 diagnoses all but the first function.  The first function isn't diagnosed because the conditionals are folded away and all the warning sees is an unconditional call to bar().  Without optimization, the member functions aren't inlined and so the warning doesn't see the uninitialized reads.  That's mostly unavoidable.  The warning does have logic to trigger when an uninitialized object is passed to a const-qualified reference (or pointer) but not if the object is passed to a non-const argument by reference prior to that.

$ gcc -O2 -S -Wall pr79658-c4.C 
pr79658-c4.C: In function ‘void foo_1(int)’:
pr79658-c4.C:40:10: warning: ‘*(unsigned int*)((char*)&f1 + offsetof(eflags, enum_flags<flag>::m_enum_value))’ may be used uninitialized [-Wmaybe-uninitialized]
   40 |   eflags f1;
      |          ^~
pr79658-c4.C: In function ‘void foo_2(int)’:
pr79658-c4.C:52:10: warning: ‘*(unsigned int*)((char*)&f2 + offsetof(eflags, enum_flags<flag>::m_enum_value))’ may be used uninitialized [-Wmaybe-uninitialized]
   52 |   eflags f2;
      |          ^~
pr79658-c4.C: In function ‘void foo_3(int)’:
pr79658-c4.C:64:10: warning: ‘*(unsigned int*)((char*)&f3 + offsetof(eflags, enum_flags<flag>::m_enum_value))’ may be used uninitialized [-Wmaybe-uninitialized]
   64 |   eflags f3;
      |          ^~

Finally, for the test case in comment #7, GCC 11 (and prior, including 9, since r264078) diagnoses all four functions:

$ gcc -O2 -S -Wall pr79658.c
pr79658.c: In function ‘foo_0’:
pr79658.c:13:21: warning: ‘f0.value’ is used uninitialized [-Wuninitialized]
   13 |   struct enum_flags f0; // doesn't warn
      |                     ^~
pr79658.c: In function ‘foo_1’:
pr79658.c:3:81: warning: ‘f1.value’ may be used uninitialized [-Wmaybe-uninitialized]
    3 | oid set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
      |                                                         ~~~~~~~~~^~~

pr79658.c:25:21: note: ‘f1.value’ was declared here
   25 |   struct enum_flags f1; // warns
      |                     ^~
pr79658.c: In function ‘foo_2’:
pr79658.c:3:81: warning: ‘f1.value’ may be used uninitialized [-Wmaybe-uninitialized]
    3 | oid set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
      |                                                         ~~~~~~~~~^~~

pr79658.c:25:21: note: ‘f1.value’ was declared here
   25 |   struct enum_flags f1; // warns
      |                     ^~
pr79658.c: In function ‘foo_3’:
pr79658.c:3:81: warning: ‘f3.value’ may be used uninitialized [-Wmaybe-uninitialized]
    3 | oid set_flag (struct enum_flags *f, int e) { f->value = f->value | e; }
      |                                                         ~~~~~~~~~^~~

pr79658.c:49:21: note: ‘f3.value’ was declared here
   49 |   struct enum_flags f3; // warns
      |                     ^~

With that, I'm going to resolve this as fixed (for the last test case).